Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
0636ab91ee
commit
2b5399ae01
|
|
@ -226,7 +226,7 @@
|
|||
{"name":"gitlab-styles","version":"12.0.1","platform":"ruby","checksum":"d8a302b0ab0e1f18e2d11501760f1b85c5e70b5e5ca628828a0786c7984ed133"},
|
||||
{"name":"gitlab_chronic_duration","version":"0.12.0","platform":"ruby","checksum":"0d766944d415b5c831f176871ee8625783fc0c5bfbef2d79a3a616f207ffc16d"},
|
||||
{"name":"gitlab_omniauth-ldap","version":"2.2.0","platform":"ruby","checksum":"bb4d20acb3b123ed654a8f6a47d3fac673ece7ed0b6992edb92dca14bad2838c"},
|
||||
{"name":"gitlab_quality-test_tooling","version":"1.32.0","platform":"ruby","checksum":"72b7bb243d3e1f8006bef4bcf3585480a774f72bb9dd97bfcc52db4b32fd30fb"},
|
||||
{"name":"gitlab_quality-test_tooling","version":"1.32.1","platform":"ruby","checksum":"6918e2048d25124583643d18d9d5cec7958976d98166a0affb5ee0386ce8be24"},
|
||||
{"name":"globalid","version":"1.1.0","platform":"ruby","checksum":"b337e1746f0c8cb0a6c918234b03a1ddeb4966206ce288fbb57779f59b2d154f"},
|
||||
{"name":"gon","version":"6.4.0","platform":"ruby","checksum":"e3a618d659392890f1aa7db420f17c75fd7d35aeb5f8fe003697d02c4b88d2f0"},
|
||||
{"name":"google-apis-androidpublisher_v3","version":"0.34.0","platform":"ruby","checksum":"d7e1d7dd92f79c498fe2082222a1740d788e022e660c135564b3fd299cab5425"},
|
||||
|
|
|
|||
|
|
@ -746,7 +746,7 @@ GEM
|
|||
omniauth (>= 1.3, < 3)
|
||||
pyu-ruby-sasl (>= 0.0.3.3, < 0.1)
|
||||
rubyntlm (~> 0.5)
|
||||
gitlab_quality-test_tooling (1.32.0)
|
||||
gitlab_quality-test_tooling (1.32.1)
|
||||
activesupport (>= 7.0, < 7.2)
|
||||
amatch (~> 0.4.1)
|
||||
gitlab (~> 4.19)
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-testid="issuable-create-actions" class="footer-block gl-display-flex gl-mt-6">
|
||||
<div data-testid="issuable-create-actions" class="footer-block gl-mt-6 gl-flex">
|
||||
<slot
|
||||
name="actions"
|
||||
:issuable-title="issuableTitle"
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form-group class="row" label-class="gl-display-none">
|
||||
<label class="col-12 gl-display-flex gl-align-center">
|
||||
<gl-form-group class="row" label-class="gl-hidden">
|
||||
<label class="col-12 gl-align-center gl-flex">
|
||||
{{ $options.i18n.fieldLabel }}
|
||||
</label>
|
||||
<div class="col-12">
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export default {
|
|||
class="issues-bulk-update right-sidebar"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div class="gl-display-flex gl-justify-content-space-between gl-p-4 gl-border-b">
|
||||
<div class="gl-border-b gl-flex gl-justify-between gl-p-4">
|
||||
<slot name="bulk-edit-actions"></slot>
|
||||
</div>
|
||||
<slot name="sidebar-items"></slot>
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ export default {
|
|||
<template>
|
||||
<li
|
||||
:id="`issuable_${issuableId}`"
|
||||
class="issue gl-display-flex! gl-px-5!"
|
||||
class="issue !gl-flex !gl-px-5"
|
||||
:class="{ closed: issuable.closedAt, 'gl-bg-blue-50': isActive }"
|
||||
:data-labels="labelIdsString"
|
||||
:data-qa-issue-id="issuableId"
|
||||
|
|
@ -292,7 +292,7 @@ export default {
|
|||
<work-item-prefetch :work-item-iid="issuableIid" data-testid="issuable-prefetch-trigger">
|
||||
<template #default="{ prefetchWorkItem, clearPrefetching }">
|
||||
<gl-link
|
||||
class="issue-title-text gl-font-base"
|
||||
class="issue-title-text gl-text-base"
|
||||
dir="auto"
|
||||
:href="issuableLinkHref"
|
||||
data-testid="issuable-title-link"
|
||||
|
|
@ -309,7 +309,7 @@ export default {
|
|||
</template>
|
||||
<template v-else>
|
||||
<gl-link
|
||||
class="issue-title-text gl-font-base"
|
||||
class="issue-title-text gl-text-base"
|
||||
dir="auto"
|
||||
:href="issuableLinkHref"
|
||||
data-testid="issuable-title-link"
|
||||
|
|
@ -323,7 +323,7 @@ export default {
|
|||
<slot v-if="hasSlotContents('title-icons')" name="title-icons"></slot>
|
||||
<span
|
||||
v-if="taskStatus"
|
||||
class="task-status gl-hidden sm:!gl-inline-block gl-ml-2 gl-font-sm"
|
||||
class="task-status gl-ml-2 gl-hidden gl-text-sm sm:!gl-inline-block"
|
||||
data-testid="task-status"
|
||||
>
|
||||
{{ taskStatus }}
|
||||
|
|
@ -360,7 +360,7 @@ export default {
|
|||
:data-avatar-url="author.avatarUrl"
|
||||
:href="author.webPath"
|
||||
data-testid="issuable-author"
|
||||
class="author-link js-user-link gl-font-sm gl-text-gray-500!"
|
||||
class="author-link js-user-link gl-text-sm !gl-text-gray-500"
|
||||
>
|
||||
<span class="author">{{ author.name }}</span>
|
||||
</gl-link>
|
||||
|
|
@ -384,7 +384,7 @@ export default {
|
|||
v-if="labels.length"
|
||||
role="group"
|
||||
:aria-label="__('Labels')"
|
||||
class="gl-mt-1 gl-mb-0 gl-display-flex gl-flex-wrap gl-gap-2"
|
||||
class="gl-mb-0 gl-mt-1 gl-flex gl-flex-wrap gl-gap-2"
|
||||
>
|
||||
<gl-label
|
||||
v-for="(label, index) in labels"
|
||||
|
|
@ -401,30 +401,30 @@ export default {
|
|||
<div class="issuable-meta">
|
||||
<ul v-if="showIssuableMeta" class="controls gl-gap-3">
|
||||
<!-- eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots -->
|
||||
<li v-if="$slots.status" data-testid="issuable-status" class="gl-mr-0!">
|
||||
<li v-if="$slots.status" data-testid="issuable-status" class="!gl-mr-0">
|
||||
<gl-badge v-if="isNotOpen" :variant="statusBadgeVariant">
|
||||
<slot name="status"></slot>
|
||||
</gl-badge>
|
||||
<slot v-else name="status"></slot>
|
||||
</li>
|
||||
<slot name="pipeline-status"></slot>
|
||||
<li v-if="assignees.length" class="gl-mr-0!">
|
||||
<li v-if="assignees.length" class="!gl-mr-0">
|
||||
<issuable-assignees
|
||||
:assignees="assignees"
|
||||
:icon-size="16"
|
||||
:max-visible="4"
|
||||
class="gl-align-items-center gl-display-flex"
|
||||
class="gl-flex gl-items-center"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
v-if="showDiscussions && notesCount"
|
||||
class="gl-hidden sm:gl-block gl-mr-0!"
|
||||
class="!gl-mr-0 gl-hidden sm:gl-block"
|
||||
data-testid="issuable-comments"
|
||||
>
|
||||
<div
|
||||
v-gl-tooltip.top
|
||||
:title="__('Comments')"
|
||||
class="!gl-text-inherit gl-display-flex gl-align-items-center"
|
||||
class="gl-flex gl-items-center !gl-text-inherit"
|
||||
>
|
||||
<gl-icon name="comments" class="gl-mr-2" />
|
||||
{{ notesCount }}
|
||||
|
|
@ -434,7 +434,7 @@ export default {
|
|||
</ul>
|
||||
<div
|
||||
v-gl-tooltip.bottom
|
||||
class="gl-text-gray-500 gl-hidden sm:gl-inline-block"
|
||||
class="gl-hidden gl-text-gray-500 sm:gl-inline-block"
|
||||
:title="tooltipTitle(timestamp)"
|
||||
data-testid="issuable-timestamp"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ export default {
|
|||
:checkbox-checked="allIssuablesChecked"
|
||||
:show-friendly-text="showFilteredSearchFriendlyText"
|
||||
terms-as-tokens
|
||||
class="gl-grow gl-border-t-0 row-content-block max-md:-gl-ml-5 max-md:gl-w-screen max-md:gl-border-t max-md:-gl-mt-1 sm:gl-flex"
|
||||
class="row-content-block gl-grow gl-border-t-0 max-md:gl-border-t max-md:-gl-ml-5 max-md:-gl-mt-1 max-md:gl-w-screen sm:gl-flex"
|
||||
data-testid="issuable-search-container"
|
||||
@checked-input="handleAllIssuablesCheckedInput"
|
||||
@onFilter="$emit('filter', $event)"
|
||||
|
|
@ -355,7 +355,7 @@ export default {
|
|||
</issuable-bulk-edit-sidebar>
|
||||
<slot name="list-body"></slot>
|
||||
<ul v-if="issuablesLoading" class="content-list">
|
||||
<li v-for="n in skeletonItemCount" :key="n" class="issue gl-px-5! gl-py-5!">
|
||||
<li v-for="n in skeletonItemCount" :key="n" class="issue !gl-px-5 !gl-py-5">
|
||||
<gl-skeleton-loader />
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -415,9 +415,7 @@ export default {
|
|||
<slot v-else-if="!error" name="empty-state"></slot>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="gl-display-flex gl-justify-content-space-between gl-md-justify-content-center! gl-mt-6 gl-relative"
|
||||
>
|
||||
<div class="gl-relative gl-mt-6 gl-flex gl-justify-between md:!gl-justify-center">
|
||||
<gl-keyset-pagination
|
||||
v-if="showPaginationControls && useKeysetPagination"
|
||||
:has-next-page="hasNextPage"
|
||||
|
|
@ -445,7 +443,7 @@ export default {
|
|||
>
|
||||
<page-size-selector
|
||||
:value="defaultPageSize"
|
||||
class="gl-right-0 gl-relative md:gl-absolute"
|
||||
class="gl-relative gl-right-0 md:gl-absolute"
|
||||
@input="handlePageSizeChange"
|
||||
/>
|
||||
</local-storage-sync>
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export default {
|
|||
<template>
|
||||
<div class="top-area">
|
||||
<gl-tabs
|
||||
class="gl-display-flex gl-flex-grow-1 gl-p-0 gl-m-0 mobile-separator issuable-state-filters"
|
||||
class="mobile-separator issuable-state-filters gl-m-0 gl-flex gl-grow gl-p-0"
|
||||
nav-class="gl-border-b-0"
|
||||
>
|
||||
<gl-tab
|
||||
|
|
|
|||
|
|
@ -186,12 +186,12 @@ export default {
|
|||
:task-list-update-path="taskListUpdatePath"
|
||||
/>
|
||||
<slot name="secondary-content"></slot>
|
||||
<small v-if="isUpdated" class="edited-text gl-font-sm! gl-text-secondary">
|
||||
<small v-if="isUpdated" class="edited-text !gl-text-sm gl-text-secondary">
|
||||
{{ __('Edited') }}
|
||||
<time-ago-tooltip :time="issuable.updatedAt" tooltip-placement="bottom" />
|
||||
<span v-if="updatedBy">
|
||||
{{ __('by') }}
|
||||
<gl-link :href="updatedBy.webUrl" class="author-link gl-font-sm! gl-text-secondary">
|
||||
<gl-link :href="updatedBy.webUrl" class="author-link !gl-text-sm gl-text-secondary">
|
||||
<span>{{ updatedBy.name }}</span>
|
||||
</gl-link>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export default {
|
|||
ref="textarea"
|
||||
:value="issuable.description"
|
||||
:data-update-url="taskListUpdatePath"
|
||||
class="gl-display-none js-task-list-field"
|
||||
class="js-task-list-field gl-hidden"
|
||||
dir="auto"
|
||||
>
|
||||
</textarea>
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ export default {
|
|||
:label="__('Description')"
|
||||
:label-sr-only="!showFieldTitle"
|
||||
label-for="issuable-description"
|
||||
class="col-12 gl-px-0 common-note-form"
|
||||
class="col-12 common-note-form gl-px-0"
|
||||
>
|
||||
<markdown-editor
|
||||
v-model="description"
|
||||
|
|
@ -152,10 +152,7 @@ export default {
|
|||
@keydown="handleKeydown($event, 'description')"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<div
|
||||
data-testid="actions"
|
||||
class="col-12 gl-mt-3 gl-mb-3 gl-px-0 clearfix gl-display-flex gl-gap-3"
|
||||
>
|
||||
<div data-testid="actions" class="col-12 clearfix gl-mb-3 gl-mt-3 gl-flex gl-gap-3 gl-px-0">
|
||||
<slot
|
||||
name="edit-form-actions"
|
||||
:issuable-title="title"
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="detail-page-header gl-flex-direction-column gl-md-flex-direction-row">
|
||||
<div class="detail-page-header gl-flex-col md:gl-flex-row">
|
||||
<div class="detail-page-header-body gl-flex-wrap gl-gap-x-2">
|
||||
<gl-badge :variant="badgeVariant" :icon="statusIcon" data-testid="issue-state-badge">
|
||||
<slot name="status-badge">{{ badgeText }}</slot>
|
||||
|
|
@ -189,7 +189,7 @@ export default {
|
|||
{{ serviceDeskReplyTo }}
|
||||
</template>
|
||||
<template #author>
|
||||
<gl-link class="gl-font-bold js-user-link" :href="author.webUrl" :data-user-id="authorId">
|
||||
<gl-link class="js-user-link gl-font-bold" :href="author.webUrl" :data-user-id="authorId">
|
||||
<span :class="[{ 'gl-hidden': !isAuthorExternal }, 'sm:gl-inline']">
|
||||
{{ author.name }}
|
||||
</span>
|
||||
|
|
@ -218,12 +218,12 @@ export default {
|
|||
>
|
||||
<gl-button
|
||||
icon="chevron-double-lg-left"
|
||||
class="gl-ml-auto gl-block sm:!gl-hidden js-sidebar-toggle"
|
||||
class="js-sidebar-toggle gl-ml-auto gl-block sm:!gl-hidden"
|
||||
:aria-label="__('Expand sidebar')"
|
||||
@click="handleRightSidebarToggleClick"
|
||||
/>
|
||||
</div>
|
||||
<div class="detail-page-header-actions gl-align-self-center gl-display-flex gl-gap-3">
|
||||
<div class="detail-page-header-actions gl-flex gl-gap-3 gl-self-center">
|
||||
<slot name="header-actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export default {
|
|||
<div class="title-container">
|
||||
<h1
|
||||
v-safe-html="issuable.titleHtml || issuable.title"
|
||||
class="title gl-font-size-h-display"
|
||||
class="title gl-text-size-h-display"
|
||||
dir="auto"
|
||||
data-testid="issuable-title"
|
||||
></h1>
|
||||
|
|
@ -87,11 +87,11 @@ export default {
|
|||
<transition name="issuable-header-slide">
|
||||
<div
|
||||
v-if="stickyTitleVisible"
|
||||
class="issue-sticky-header gl-fixed gl-z-3 gl-bg-default gl-border-b gl-py-3"
|
||||
class="issue-sticky-header gl-border-b gl-fixed gl-z-3 gl-bg-default gl-py-3"
|
||||
data-testid="header"
|
||||
>
|
||||
<div class="issue-sticky-header-text gl-flex gl-items-baseline gl-mx-auto gl-gap-3">
|
||||
<gl-badge class="gl-whitespace-nowrap gl-self-center" :variant="badgeVariant">
|
||||
<div class="issue-sticky-header-text gl-mx-auto gl-flex gl-items-baseline gl-gap-3">
|
||||
<gl-badge class="gl-self-center gl-whitespace-nowrap" :variant="badgeVariant">
|
||||
<gl-icon v-if="statusIcon" class="sm:gl-hidden" :name="statusIcon" />
|
||||
<span class="gl-sr-only sm:gl-not-sr-only">
|
||||
<slot name="status-badge"></slot>
|
||||
|
|
@ -104,7 +104,7 @@ export default {
|
|||
:workspace-type="workspaceType"
|
||||
/>
|
||||
<p
|
||||
class="gl-font-bold gl-overflow-hidden gl-whitespace-nowrap gl-text-overflow-ellipsis gl-my-0"
|
||||
class="gl-my-0 gl-overflow-hidden gl-text-ellipsis gl-whitespace-nowrap gl-font-bold"
|
||||
:title="issuable.title"
|
||||
>
|
||||
{{ issuable.title }}
|
||||
|
|
|
|||
|
|
@ -292,10 +292,6 @@ module ApplicationHelper
|
|||
"#{request.path}?#{options.compact.to_param}"
|
||||
end
|
||||
|
||||
def stylesheet_link_tag_defer(path)
|
||||
universal_stylesheet_link_tag(path, media: "all", crossorigin: ActionController::Base.asset_host ? 'anonymous' : nil)
|
||||
end
|
||||
|
||||
def sign_in_with_redirect?
|
||||
current_page?(new_user_session_path) && session[:user_return_to].present?
|
||||
end
|
||||
|
|
@ -419,17 +415,13 @@ module ApplicationHelper
|
|||
}
|
||||
end
|
||||
|
||||
def add_page_specific_style(path, defer: true)
|
||||
def add_page_specific_style(path)
|
||||
@already_added_styles ||= Set.new
|
||||
return if @already_added_styles.include?(path)
|
||||
|
||||
@already_added_styles.add(path)
|
||||
content_for :page_specific_styles do
|
||||
if defer
|
||||
stylesheet_link_tag_defer path
|
||||
else
|
||||
universal_stylesheet_link_tag path
|
||||
end
|
||||
universal_stylesheet_link_tag path
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
module Taggable
|
||||
extend ActiveSupport::Concern
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
included do
|
||||
after_save :save_tags
|
||||
|
||||
# rubocop:disable Cop/ActiveRecordDependent -- existing
|
||||
has_many :tag_taggings, -> { includes(:tag).where(context: :tags) }, # rubocop:disable Rails/InverseOf -- existing
|
||||
as: :taggable,
|
||||
class_name: 'Ci::Tagging',
|
||||
dependent: :destroy,
|
||||
after_add: :dirtify_tag_list,
|
||||
after_remove: :dirtify_tag_list
|
||||
|
||||
has_many :tags,
|
||||
class_name: 'Ci::Tag',
|
||||
through: :tag_taggings,
|
||||
source: :tag,
|
||||
after_add: :dirtify_tag_list,
|
||||
after_remove: :dirtify_tag_list
|
||||
|
||||
has_many :taggings, as: :taggable, dependent: :destroy, class_name: '::Ci::Tagging'
|
||||
has_many :base_tags, through: :taggings, source: :tag, class_name: '::Ci::Tag'
|
||||
# rubocop:enable Cop/ActiveRecordDependent -- existing
|
||||
|
||||
attribute :tag_list, Gitlab::Database::Type::TagListType.new
|
||||
|
||||
scope :tagged_with, ->(tags) do
|
||||
Gitlab::Ci::Tags::Parser
|
||||
.new(tags)
|
||||
.parse
|
||||
.map { |tag| with_tag(tag) }
|
||||
.reduce(:and)
|
||||
end
|
||||
|
||||
scope :with_tag, ->(name) do
|
||||
where_exists(
|
||||
Tagging
|
||||
.merge(unscoped.scoped_tagging)
|
||||
.where(context: :tags)
|
||||
.where(tag_id: Tag.where(name: name))
|
||||
)
|
||||
end
|
||||
|
||||
scope :scoped_tagging, -> do
|
||||
where(arel_table[primary_key].eq(Tagging.arel_table[:taggable_id]))
|
||||
.where(Tagging.arel_table[:taggable_type].eq(base_class.name))
|
||||
end
|
||||
end
|
||||
|
||||
def tag_list
|
||||
Gitlab::Ci::Tags::TagList.new(context_tags.map(&:name))
|
||||
end
|
||||
strong_memoize_attr :tag_list
|
||||
|
||||
def tag_list=(new_tags)
|
||||
parsed_new_list = Gitlab::Ci::Tags::Parser.new(new_tags).parse
|
||||
write_attribute('tag_list', parsed_new_list)
|
||||
instance_variable_set(:@tag_list, parsed_new_list)
|
||||
end
|
||||
|
||||
def reload(*args)
|
||||
clear_memoization(:tag_list)
|
||||
super(*args)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dirtify_tag_list(_tag)
|
||||
attribute_will_change!(:tag_list)
|
||||
clear_memoization(:tag_list)
|
||||
end
|
||||
|
||||
def context_tags
|
||||
base_tags.where(taggings: { context: :tags, tagger_id: nil })
|
||||
end
|
||||
|
||||
def tag_list_cache_set?
|
||||
strong_memoized?(:tag_list) && tag_list.any?
|
||||
end
|
||||
|
||||
def save_tags
|
||||
return unless tag_list_cache_set?
|
||||
|
||||
tags = find_or_create_tags_from_list(tag_list.uniq)
|
||||
current_tags = context_tags
|
||||
old_tags = current_tags - tags
|
||||
new_tags = tags - current_tags
|
||||
|
||||
taggings.by_context(:tags).where(tag_id: old_tags).delete_all if old_tags.present?
|
||||
|
||||
new_tags.each do |tag|
|
||||
taggings.create!(tag_id: tag.id, context: 'tags', taggable: self)
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def find_or_create_tags_from_list(tags)
|
||||
Ci::Tag.find_or_create_all_with_like_by_name(tags)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -12,6 +12,8 @@ class RedirectRoute < MainClusterwide::ApplicationRecord
|
|||
presence: true,
|
||||
uniqueness: { case_sensitive: false }
|
||||
|
||||
scope :for_source_type, ->(source_type) { where(source_type: source_type) }
|
||||
scope :by_paths, ->(paths) { where(path: [paths]) }
|
||||
scope :matching_path_and_descendants, ->(path) do
|
||||
wheres = 'LOWER(redirect_routes.path) = LOWER(?) OR LOWER(redirect_routes.path) LIKE LOWER(?)'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
- add_to_breadcrumbs _("Groups"), admin_groups_path
|
||||
- add_to_breadcrumbs @group.name, admin_group_path(@group)
|
||||
- breadcrumb_title _("Edit")
|
||||
- page_title _("Edit"), @group.name, _("Groups")
|
||||
|
||||
%h1.page-title.gl-font-size-h-display= _('Edit group: %{group_name}') % { group_name: @group.name }
|
||||
%hr
|
||||
= render 'form', visibility_level: @group.visibility_level
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
- add_to_breadcrumbs _("Projects"), admin_projects_path
|
||||
- add_to_breadcrumbs @project.full_name, admin_project_path(@project)
|
||||
- breadcrumb_title _("Edit")
|
||||
- page_title _("Edit"), @project.name, _("Projects")
|
||||
|
||||
%h1.page-title.gl-font-size-h-display= _('Edit project: %{project_name}') % { project_name: @project.name }
|
||||
%hr
|
||||
= render 'form'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
- add_to_breadcrumbs _("Topics"), admin_topics_path
|
||||
- add_to_breadcrumbs @topic.name, edit_admin_topic_path(@topic) # There is no show view, so we reuse edit here
|
||||
- breadcrumb_title _("Edit")
|
||||
- page_title _("Edit"), @topic.name, _("Topics")
|
||||
|
||||
%h1.page-title.gl-font-size-h-display= _('Edit topic: %{topic_name}') % { topic_name: @topic.name }
|
||||
%hr
|
||||
= render 'form', url: admin_topic_path(@topic)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
- add_to_breadcrumbs _("Users"), admin_users_path
|
||||
- add_to_breadcrumbs @user.name, admin_user_path(@user)
|
||||
- breadcrumb_title _("Edit")
|
||||
- page_title _("Edit"), @user.name, _("Users")
|
||||
|
||||
%h1.page-title.gl-font-size-h-display
|
||||
= _("Edit user: %{user_name}") % { user_name: @user.name }
|
||||
= render 'form'
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
= webpack_bundle_tag 'jira_connect_app'
|
||||
|
||||
- add_page_specific_style 'page_bundles/jira_connect', defer: false
|
||||
- add_page_specific_style 'page_bundles/jira_connect'
|
||||
- add_page_specific_style 'tailwind'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
- addition_color = local_assigns.fetch(:addition, nil)
|
||||
|
||||
- if deletion_color.present? || request.path == profile_preferences_path
|
||||
= stylesheet_link_tag_defer "highlight/diff_custom_colors_deletion"
|
||||
= universal_stylesheet_link_tag "highlight/diff_custom_colors_deletion"
|
||||
- if deletion_color.present?
|
||||
- deletion_color_rgb = hex_color_to_rgb_array(deletion_color).join(',')
|
||||
:css
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
}
|
||||
|
||||
- if addition_color.present? || request.path == profile_preferences_path
|
||||
= stylesheet_link_tag_defer "highlight/diff_custom_colors_addition"
|
||||
= universal_stylesheet_link_tag "highlight/diff_custom_colors_addition"
|
||||
- if addition_color.present?
|
||||
- addition_color_rgb = hex_color_to_rgb_array(addition_color).join(',')
|
||||
:css
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@
|
|||
|
||||
- if user_application_dark_mode?
|
||||
%meta{ name: 'color-scheme', content: 'dark light' }
|
||||
= stylesheet_link_tag_defer "application_dark"
|
||||
= universal_stylesheet_link_tag "application_dark"
|
||||
= yield :page_specific_styles
|
||||
= stylesheet_link_tag_defer "application_utilities_dark"
|
||||
= universal_stylesheet_link_tag "application_utilities_dark"
|
||||
- elsif user_application_system_mode?
|
||||
%meta{ name: 'color-scheme', content: 'light dark' }
|
||||
= universal_stylesheet_link_tag "application", media: "(prefers-color-scheme: light)"
|
||||
|
|
@ -42,16 +42,16 @@
|
|||
= universal_stylesheet_link_tag "application_utilities", media: "(prefers-color-scheme: light)"
|
||||
= universal_stylesheet_link_tag "application_utilities_dark", media: "(prefers-color-scheme: dark)"
|
||||
- else
|
||||
= stylesheet_link_tag_defer "application"
|
||||
= universal_stylesheet_link_tag "application"
|
||||
= yield :page_specific_styles
|
||||
= stylesheet_link_tag_defer 'application_utilities'
|
||||
= stylesheet_link_tag_defer 'tailwind'
|
||||
= universal_stylesheet_link_tag 'application_utilities'
|
||||
= universal_stylesheet_link_tag 'tailwind'
|
||||
= universal_stylesheet_link_tag "disable_animations", media: "all" if Rails.env.test? || Gitlab.config.gitlab['disable_animations']
|
||||
= universal_stylesheet_link_tag "test_environment", media: "all" if Rails.env.test?
|
||||
|
||||
= stylesheet_link_tag_defer "fonts"
|
||||
= universal_stylesheet_link_tag "fonts"
|
||||
|
||||
= stylesheet_link_tag_defer "highlight/themes/#{user_color_scheme}"
|
||||
= universal_stylesheet_link_tag "highlight/themes/#{user_color_scheme}"
|
||||
|
||||
= universal_stylesheet_link_tag 'performance_bar' if performance_bar_enabled?
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
- else
|
||||
= universal_stylesheet_link_tag "application_dark"
|
||||
= universal_stylesheet_link_tag "application_utilities_dark"
|
||||
= universal_stylesheet_link_tag 'tailwind'
|
||||
|
||||
%body
|
||||
.gl-mt-6{ class: (params[:lookbook][:display][:layout] == "fluid" ? "container-fluid" : "container") }
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: group_level_vulnerability_report_grouping
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137778
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/432778
|
||||
milestone: '16.7'
|
||||
type: development
|
||||
group: group::threat insights
|
||||
default_enabled: true
|
||||
|
|
@ -665,7 +665,14 @@ class FixSequenceOwnersForCiBuilds < Gitlab::Database::Migration[2.2]
|
|||
sequences = find_sequences_owned_by_ci_builds.to_a.pluck('seq_name') - ['ci_builds_id_seq']
|
||||
|
||||
sequences.each do |seq_name|
|
||||
owner = SEQUENCES.fetch(seq_name)
|
||||
owner = SEQUENCES[seq_name]
|
||||
|
||||
# Ignore unknown sequences: it's possible that a sequence was
|
||||
# assigned the wrong owner due to a bug in `sequences_owned_by`
|
||||
# (fixed by https://gitlab.com/gitlab-org/gitlab/-/merge_requests/160528),
|
||||
# and so it didn't get dropped automatically when its associated
|
||||
# table was dropped: https://gitlab.com/gitlab-org/gitlab/-/issues/474293
|
||||
next unless owner
|
||||
|
||||
with_lock_retries do
|
||||
execute(<<~SQL.squish)
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@ configuration option in `gitlab.yml`. These metrics are served from the
|
|||
| `sidekiq_mem_total_bytes` | Gauge | 15.3 | Number of bytes allocated for both objects consuming an object slot and objects that required a malloc'| |
|
||||
| `sidekiq_concurrency_limit_queue_jobs` | Gauge | 17.3 | Number of Sidekiq jobs waiting in the concurrency limit queue| `worker` |
|
||||
| `sidekiq_concurrency_limit_max_concurrent_jobs` | Gauge | 17.3 | Max number of concurrent running Sidekiq jobs | `worker` |
|
||||
| `sidekiq_concurrency_limit_deferred_jobs_total` | Counter | 17.3 | Total number of deferred Sidekiq jobs | `worker` |
|
||||
| `geo_db_replication_lag_seconds` | Gauge | 10.2 | Database replication lag (seconds) | `url` |
|
||||
| `geo_repositories` | Gauge | 10.2 | Total number of repositories available on primary | `url` |
|
||||
| `geo_lfs_objects` | Gauge | 10.2 | Number of LFS objects on primary | `url` |
|
||||
|
|
|
|||
|
|
@ -1264,15 +1264,15 @@ Parameters:
|
|||
| `room` | string | yes | Unique identifier for the target room (in the format `!qPKKM111FFKKsfoCVy:matrix.org`). |
|
||||
| `notify_only_broken_pipelines` | boolean | no | Send notifications for broken pipelines. |
|
||||
| `branches_to_be_notified` | string | no | Branches to send notifications for. Valid options are `all`, `default`, `protected`, and `default_and_protected`. The default value is `default`. |
|
||||
| `push_events` | boolean | yes | Enable notifications for push events. |
|
||||
| `issues_events` | boolean | yes | Enable notifications for issue events. |
|
||||
| `confidential_issues_events` | boolean | yes | Enable notifications for confidential issue events. |
|
||||
| `merge_requests_events` | boolean | yes | Enable notifications for merge request events. |
|
||||
| `tag_push_events` | boolean | yes | Enable notifications for tag push events. |
|
||||
| `note_events` | boolean | yes | Enable notifications for note events. |
|
||||
| `confidential_note_events` | boolean | yes | Enable notifications for confidential note events. |
|
||||
| `pipeline_events` | boolean | yes | Enable notifications for pipeline events. |
|
||||
| `wiki_page_events` | boolean | yes | Enable notifications for wiki page events. |
|
||||
| `push_events` | boolean | no | Enable notifications for push events. |
|
||||
| `issues_events` | boolean | no | Enable notifications for issue events. |
|
||||
| `confidential_issues_events` | boolean | no | Enable notifications for confidential issue events. |
|
||||
| `merge_requests_events` | boolean | no | Enable notifications for merge request events. |
|
||||
| `tag_push_events` | boolean | no | Enable notifications for tag push events. |
|
||||
| `note_events` | boolean | no | Enable notifications for note events. |
|
||||
| `confidential_note_events` | boolean | no | Enable notifications for confidential note events. |
|
||||
| `pipeline_events` | boolean | no | Enable notifications for pipeline events. |
|
||||
| `wiki_page_events` | boolean | no | Enable notifications for wiki page events. |
|
||||
| `use_inherited_settings` | boolean | no | Indicates whether or not to inherit default settings. Defaults to `false`. |
|
||||
|
||||
### Disable Matrix notifications
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ PUT /projects/:id/external_status_checks/:check_id
|
|||
| `check_id` | integer | yes | ID of an external status check service |
|
||||
| `name` | string | no | Display name of external status check service |
|
||||
| `external_url` | string | no | URL of external status check service |
|
||||
| `shared_secret` | string | no | HMAC secret for external status check |
|
||||
| `protected_branch_ids` | `array<Integer>` | no | IDs of protected branches to scope the rule by |
|
||||
|
||||
## Delete external status check service
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ For CI jobs where `FF_TIMESTAMPS` is enabled,
|
|||
the size of generated artifacts is bigger by approximately 10 percent.
|
||||
|
||||
NOTE:
|
||||
If `FF_TIMESTMPS` is disabled or not included in `.gitlab-ci.yml`,
|
||||
If `FF_TIMESTAMPS` is disabled or not included in `.gitlab-ci.yml`,
|
||||
the CI log output does not include the timestamp for each log line.
|
||||
|
||||
## Example configuration and output
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ myFancyToggleToOpenChat.addEventListener('click', () => {
|
|||
#### Initiating GitLab Duo Chat with a pre-defined prompt
|
||||
|
||||
In some scenarios, you may want to direct users towards a specific topic or
|
||||
query when they open GitLab Duo Chat. We have a utility function that will
|
||||
query when they open GitLab Duo Chat. We have a utility function that will
|
||||
open DuoChat drawer and send a command in a queue for DuoChat to execute on.
|
||||
This should trigger the loading state and the streaming with the given prompt.
|
||||
|
||||
|
|
@ -375,7 +375,7 @@ The GraphQL Subscription for Chat behaves slightly different because it's user-c
|
|||
We therefore need to broadcast messages to multiple clients to keep them in sync. The `aiAction` mutation with the `chat` action behaves the following:
|
||||
|
||||
1. All complete Chat messages (including messages from the user) are broadcasted with the `userId`, `aiAction: "chat"` as identifier.
|
||||
1. Chunks from streamed Chat messages and currently used tools are broadcasted with the `userId`, `resourceId`, and the `clientSubscriptionId` from the mutation as identifier.
|
||||
1. Chunks from streamed Chat messages are broadcasted with the `clientSubscriptionId` from the mutation as identifier.
|
||||
|
||||
Examples of GraphQL Subscriptions in a Vue component:
|
||||
|
||||
|
|
@ -417,8 +417,8 @@ Examples of GraphQL Subscriptions in a Vue component:
|
|||
query: aiResponseSubscription,
|
||||
variables() {
|
||||
return {
|
||||
aiAction: 'CHAT',
|
||||
userId, // for example "gid://gitlab/User/1"
|
||||
resourceId, // can be either a resourceId (on Issue, Epic, etc. items), or userId
|
||||
clientSubscriptionId // randomly generated identifier for every message
|
||||
htmlResponse: false, // important to bypass HTML processing on every chunk
|
||||
};
|
||||
|
|
@ -433,8 +433,7 @@ Examples of GraphQL Subscriptions in a Vue component:
|
|||
},
|
||||
```
|
||||
|
||||
Note that we still broadcast chat messages and currently used tools using the `userId` and `resourceId` as identifier.
|
||||
However, this is deprecated and should no longer be used. We want to remove `resourceId` on the subscription as part of [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/420296).
|
||||
Please keep in mind that the clientSubscriptionId must be unique for every request. Reusing a clientSubscriptionId will cause several unwanted side effects in the subscription responses.
|
||||
|
||||
### Duo Chat GraphQL queries
|
||||
|
||||
|
|
|
|||
|
|
@ -372,12 +372,18 @@ With the `concurrency_limit` property, you can limit the worker's concurrency. I
|
|||
a separate `LIST` and re-enqueued when it falls under the limit. `ConcurrencyLimit::ResumeWorker` is a cron
|
||||
worker that checks if any throttled jobs should be re-enqueued.
|
||||
|
||||
The first job that crosses the defined concurency limit initiates the throttling process for all other jobs of this class.
|
||||
The first job that crosses the defined concurrency limit initiates the throttling process for all other jobs of this class.
|
||||
Until this happens, jobs are scheduled and executed as usual.
|
||||
|
||||
When the throttling starts, newly scheduled and executed jobs will be added to the end of the `LIST` to ensure that
|
||||
the execution order is preserved. As soon as the `LIST` is empty again, the throttling process ends.
|
||||
|
||||
Prometheus metrics are exposed to monitor workers using concurrency limit middleware:
|
||||
|
||||
- `sidekiq_concurrency_limit_deferred_jobs_total`
|
||||
- `sidekiq_concurrency_limit_queue_jobs`
|
||||
- `sidekiq_concurrency_limit_max_concurrent_jobs`
|
||||
|
||||
WARNING:
|
||||
If there is a sustained workload over the limit, the `LIST` is going to grow until the limit is disabled or
|
||||
the workload drops under the limit.
|
||||
|
|
|
|||
|
|
@ -360,3 +360,4 @@ To merge changes from a local branch to a feature branch, follow this workflow.
|
|||
|
||||
- [Add file from the UI](../user/project/repository/index.md#add-a-file-from-the-ui)
|
||||
- [Add file from the Web IDE](../user/project/repository/web_editor.md#upload-a-file)
|
||||
- [Sign commits](../user/project/repository/signed_commits/gpg.md)
|
||||
|
|
|
|||
|
|
@ -61,3 +61,8 @@ To update your branch with the latest changes in the default branch, either:
|
|||
- Run `git pull <remote-name> <default-branch-name>`. Use this command when you want your changes to appear in Git logs
|
||||
in chronological order with the changes from the default branch, or if you're sharing your branch with others. If
|
||||
you're unsure of the correct value for `<remote-name>`, run: `git remote`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Branches](../../user/project/repository/branches/index.md)
|
||||
- [Tags](../../user/project/repository/tags/index.md)
|
||||
|
|
|
|||
|
|
@ -72,8 +72,44 @@ sudo apt-get update && sudo apt-get install git
|
|||
## Configure Git
|
||||
|
||||
To start using Git from your computer, you must enter your credentials
|
||||
to identify yourself as the author of your work. The full name and
|
||||
email address should match the ones you use in GitLab.
|
||||
to identify yourself as the author of your work.
|
||||
|
||||
You can configure your Git identity locally or globally:
|
||||
|
||||
- Locally: Use for the current project only.
|
||||
- Globally: Use for all current and future projects.
|
||||
|
||||
::Tabs
|
||||
|
||||
:::TabTitle Local setup
|
||||
|
||||
Configure your Git identity locally to use it for the current project only.
|
||||
|
||||
The full name and email address should match the ones you use in GitLab.
|
||||
|
||||
1. In your terminal, add your full name. For example:
|
||||
|
||||
```shell
|
||||
git config --local user.name "Alex Smith"
|
||||
```
|
||||
|
||||
1. Add your email address. For example:
|
||||
|
||||
```shell
|
||||
git config --local user.email "your_email_address@example.com"
|
||||
```
|
||||
|
||||
1. To check the configuration, run:
|
||||
|
||||
```shell
|
||||
git config --local --list
|
||||
```
|
||||
|
||||
:::TabTitle Global setup
|
||||
|
||||
Configure your Git identity globally to use it for all current and future projects on your machine.
|
||||
|
||||
The full name and email address should match the ones you use in GitLab.
|
||||
|
||||
1. In your terminal, add your full name. For example:
|
||||
|
||||
|
|
@ -93,9 +129,13 @@ email address should match the ones you use in GitLab.
|
|||
git config --global --list
|
||||
```
|
||||
|
||||
The `--global` option tells Git to always use this information for anything you do on your system.
|
||||
If you omit `--global` or use `--local`, the configuration applies only to the current
|
||||
repository.
|
||||
::EndTabs
|
||||
|
||||
To check your configured Git settings, run:
|
||||
|
||||
```shell
|
||||
git config user.name && git config user.email
|
||||
```
|
||||
|
||||
After you set your name and email address, you should add an SSH key.
|
||||
See [Use SSH keys to communicate with GitLab](../../../user/ssh.md).
|
||||
|
|
|
|||
|
|
@ -15,110 +15,14 @@ GitLab is built on top of (and with) Git, and provides you a Git-based, fully-in
|
|||
platform for software development. GitLab adds many powerful
|
||||
[features](https://about.gitlab.com/features/) on top of Git to enhance your workflow.
|
||||
|
||||
These resources can help you to get the best from using Git with GitLab.
|
||||
|
||||
## Learn about Git
|
||||
|
||||
New to Git? These resources can help you understand basic Git concepts before
|
||||
you dive in:
|
||||
|
||||
- [Git concepts](get_started.md)
|
||||
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||
GitLab workflow video tutorial: [GitLab source code management walkthrough](https://www.youtube.com/watch?v=wTQ3aXJswtM)
|
||||
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||
Git basics video tutorial: [Git-ing started with Git](https://www.youtube.com/watch?v=Ce5nz5n41z4)
|
||||
- PDF download: [GitLab Git Cheat Sheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf)
|
||||
|
||||
The official Git documentation also offers information on
|
||||
[Git basics](https://git-scm.com/book/en/v2/Getting-Started-Git-Basics).
|
||||
|
||||
## Begin using Git
|
||||
|
||||
After you learn how Git works, you're ready to try it out. These resources are
|
||||
appropriate for when you're ready to start learning Git by doing:
|
||||
|
||||
- [How to install Git](how_to_install_git/index.md)
|
||||
- Tutorial: [Make your first Git commit](../../tutorials/make_first_git_commit/index.md)
|
||||
- Tutorial: [Update Git commit messages](../../tutorials/update_commit_messages/index.md)
|
||||
- The [GitLab CLI](https://gitlab.com/gitlab-org/cli/)
|
||||
|
||||
A typical Git user encounters these concepts soon after starting to use Git:
|
||||
|
||||
- [`git add`](../../gitlab-basics/add-file.md) to start tracking files with Git.
|
||||
- [Tags](../../user/project/repository/tags/index.md) and
|
||||
[branches](../../user/project/repository/branches/index.md).
|
||||
- [How to undo changes](undo.md), including `git reset`.
|
||||
- View a chronological list of changes to a file with
|
||||
[Git history](../../user/project/repository/files/git_history.md).
|
||||
- View a line-by-line editing history of a file with
|
||||
[`git blame`](../../user/project/repository/files/git_blame.md).
|
||||
- [Sign commits](../../user/project/repository/signed_commits/gpg.md)
|
||||
for increased accountability and trust.
|
||||
|
||||
## Learn more complex commands
|
||||
|
||||
When you're comfortable with basic Git commands, you're ready to dive into the
|
||||
more complex features of Git. These commands aren't required when creating
|
||||
straightforward changes. When you begin managing multiple branches or need more complex
|
||||
change management, you're ready for these features:
|
||||
|
||||
- To stop tracking changes to a file, because you don't want to commit them,
|
||||
[unstage the changes](undo.md).
|
||||
- [Stash your changes](../../gitlab-basics/add-file.md) when your current work isn't ready to create a commit locally,
|
||||
but you need to switch branches to work on something else.
|
||||
- If you create many small commits locally, you can use
|
||||
[squash and merge](../../user/project/merge_requests/squash_and_merge.md)
|
||||
to combine them into fewer commits before pushing them.
|
||||
- [Cherry-pick](../../user/project/merge_requests/cherry_pick_changes.md) the contents
|
||||
of a commit from one branch to another.
|
||||
- [Revert an existing commit](../../user/project/merge_requests/revert_changes.md#revert-a-commit)
|
||||
if it contains changes you no longer want.
|
||||
|
||||
## Learn branching and workflow strategies
|
||||
|
||||
When you're comfortable with the creation and handling of individual branches,
|
||||
you're ready to learn about Git workflows and branching strategies:
|
||||
|
||||
- [Feature branch workflow](../../gitlab-basics/feature_branch_workflow.md)
|
||||
- [Introduction to Git rebase, force-push, and merge conflicts](git_rebase.md)
|
||||
- [GitLab Flow](https://about.gitlab.com/topics/version-control/what-is-gitlab-flow/)
|
||||
- [GitLab Flow best practices](https://about.gitlab.com/topics/version-control/what-are-gitlab-flow-best-practices/)
|
||||
- From the official Git documentation:
|
||||
- [Git Branching - Branches in a Nutshell](https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell)
|
||||
- [Git Branching - Branching Workflows](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows)
|
||||
|
||||
## Learn advanced topics in Git management
|
||||
|
||||
Git and GitLab, combined together, provide advanced features for repository management:
|
||||
|
||||
- Enforce commit policies and run tasks with [Git server hooks](../../administration/server_hooks.md).
|
||||
- Define which file types to treat as binary, and set the languages to use for
|
||||
syntax highlighting with [the `.gitattributes` file](../../user/project/git_attributes.md).
|
||||
- To keep a Git repository as a subdirectory in another repository,
|
||||
[use Git submodules with GitLab CI](../../ci/git_submodules.md).
|
||||
- When working with extremely large repositories, you can use a [partial clone](partial_clone.md)
|
||||
of a repository instead of a complete clone.
|
||||
- GitLab APIs for [`.gitignore` files](../../api/templates/gitignores.md),
|
||||
[commits](../../api/commits.md), [tags](../../api/tags.md),
|
||||
and [repositories](../../api/repositories.md).
|
||||
|
||||
### Git Large File Storage (LFS)
|
||||
|
||||
Many Git projects must manage large binary assets, such as videos and images.
|
||||
Implementing [Git Large File Storage](https://git-lfs.com) can help manage these assets while keeping
|
||||
your repository small:
|
||||
|
||||
- [User documentation](lfs/index.md) for Git LFS at GitLab
|
||||
- [Administrator documentation](../../administration/lfs/index.md) for Git LFS at GitLab
|
||||
- Blog post: [Getting Started with Git LFS](https://about.gitlab.com/blog/2017/01/30/getting-started-with-git-lfs-tutorial/)
|
||||
- [Migrate an existing Git repository](lfs/index.md#migrate-an-existing-repository-to-git-lfs) to Git LFS
|
||||
- [Stop tracking a file](lfs/index.md#stop-tracking-a-file-with-git-lfs) with Git LFS
|
||||
- Blog post: [Towards a production-quality open source Git LFS server](https://about.gitlab.com/blog/2015/08/13/towards-a-production-quality-open-source-git-lfs-server/)
|
||||
|
||||
## Related topics
|
||||
|
||||
- Official [Git documentation](https://git-scm.com), including
|
||||
[Git on the Server - GitLab](https://git-scm.com/book/en/v2/Git-on-the-Server-GitLab)
|
||||
- [Git troubleshooting](troubleshooting_git.md) techniques
|
||||
- Blog post: [Git Tips & Tricks](https://about.gitlab.com/blog/2016/12/08/git-tips-and-tricks/)
|
||||
- Blog post: [Eight Tips to help you work better with Git](https://about.gitlab.com/blog/2015/02/19/8-tips-to-help-you-work-better-with-git/)
|
||||
- [Get started](get_started.md)
|
||||
- [Install Git](how_to_install_git/index.md)
|
||||
- [Tutorial: Create your first commit](../../tutorials/make_first_git_commit/index.md)
|
||||
- [Clone a repository to your local machine](clone.md)
|
||||
- [Create a branch for your changes](branch.md)
|
||||
- [Add files and make changes](../../gitlab-basics/add-file.md)
|
||||
- [Undo changes](undo.md)
|
||||
- [Tutorial: Update Git commit messages](../../tutorials/update_commit_messages/index.md)
|
||||
- [Common Git commands](../../gitlab-basics/start-using-git.md)
|
||||
- [Rebase and force-push](git_rebase.md)
|
||||
- [Troubleshooting](troubleshooting_git.md)
|
||||
|
|
|
|||
|
|
@ -573,16 +573,10 @@ git checkout -- <file>
|
|||
git rm <filename> --cache
|
||||
```
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
## Related topics
|
||||
|
||||
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
|
||||
one might have when setting this up, or when something is changed, or on upgrading, it's
|
||||
important to describe those, too. Think of things that may go wrong and include them here.
|
||||
This is important to minimize requests for support, and to avoid doc comments with
|
||||
questions that you know someone might ask.
|
||||
|
||||
Each scenario can be a third-level heading, for example `### Getting error message X`.
|
||||
If you have none to add when creating a doc, leave this section in place
|
||||
but commented out to help encourage others to add to it in the future. -->
|
||||
|
||||
<!-- Identifiers, in alphabetical order -->
|
||||
- [`git blame`](../../user/project/repository/files/git_blame.md)
|
||||
- [Cherry-pick](../../user/project/merge_requests/cherry_pick_changes.md)
|
||||
- [Git history](../../user/project/repository/files/git_history.md)
|
||||
- [Revert an existing commit](../../user/project/merge_requests/revert_changes.md#revert-a-commit)
|
||||
- [Squash and merge](../../user/project/merge_requests/squash_and_merge.md)
|
||||
|
|
|
|||
|
|
@ -17,3 +17,5 @@ the most out of GitLab.
|
|||
| [Take advantage of Git rebase](https://about.gitlab.com/blog/2022/10/06/take-advantage-of-git-rebase/) | Learn how to use the `rebase` command in your workflow. | |
|
||||
| [Update Git commit messages](update_commit_messages/index.md) | Learn how to update commit messages and push the changes to GitLab. | |
|
||||
| [Git cheat sheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf) | Download a PDF of common Git commands. | |
|
||||
| [Git-ing started with Git](https://www.youtube.com/watch?v=Ce5nz5n41z4) | Git basics video tutorial. | |
|
||||
| [GitLab source code management walkthrough](https://www.youtube.com/watch?v=wTQ3aXJswtM) | GitLab workflow video tutorial. | |
|
||||
|
|
|
|||
|
|
@ -274,3 +274,8 @@ Nice work.
|
|||
|
||||
- Get a complete introduction to Git in the <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Git for GitLab](https://www.youtube.com/watch?v=4lxvVj7wlZw) beginner's course (1h 33m).
|
||||
- Find other tutorials about Git and GitLab on the [tutorials page](../index.md).
|
||||
- PDF download: [GitLab Git Cheat Sheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf).
|
||||
- Blog post: [Git Tips & Tricks](https://about.gitlab.com/blog/2016/12/08/git-tips-and-tricks/).
|
||||
- Blog post: [Eight Tips to help you work better with Git](https://about.gitlab.com/blog/2015/02/19/8-tips-to-help-you-work-better-with-git/).
|
||||
- Official [Git documentation](https://git-scm.com), including
|
||||
[Git on the Server - GitLab](https://git-scm.com/book/en/v2/Git-on-the-Server-GitLab).
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ To filter the list of vulnerabilities:
|
|||
> - Project-level grouping of vulnerabilities [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/422509) in GitLab 16.6. Feature flag `vulnerability_report_grouping` removed.
|
||||
> - Group-level grouping of vulnerabilities [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137778) in GitLab 16.7 with a flag named [`group_level_vulnerability_report_grouping`](https://gitlab.com/gitlab-org/gitlab/-/issues/432778). Disabled by default.
|
||||
> - Group-level grouping of vulnerabilities [enabled on GitLab.com, self-managed, and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157949) in GitLab 17.2.
|
||||
> - Group-level grouping of vulnerabilities [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/472669) in GitLab 17.3. Feature flag `group_level_vulnerability_report_grouping` removed.
|
||||
|
||||
You can group vulnerabilities on the vulnerability report page to more efficiently triage them.
|
||||
|
||||
|
|
@ -193,9 +194,9 @@ To group vulnerabilities:
|
|||
|
||||
1. On the left sidebar, select **Search or go to** and find your project or group.
|
||||
1. Select **Secure > Vulnerability report**.
|
||||
1. From the **Group By** dropdown list, select an attribute.
|
||||
1. From the **Group By** dropdown list, select a group.
|
||||
|
||||
Vulnerabilities are grouped according to the attribute you selected. Each group is collapsed, with
|
||||
Vulnerabilities are grouped according to the group you selected. Each group is collapsed, with
|
||||
the total number of vulnerabilities per group displayed beside their name. To see the
|
||||
vulnerabilities in each group, select the group's name.
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ DETAILS:
|
|||
FLAG:
|
||||
The availability of this feature is controlled by a feature flag.
|
||||
For more information, see the history.
|
||||
This feature is available for testing, but not ready for production use.
|
||||
This feature is currently in development and not ready for production use.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
|
|
|
|||
|
|
@ -518,10 +518,14 @@ Users with the Minimal Access role do not:
|
|||
Owners must explicitly add these users to the specific subgroups and
|
||||
projects.
|
||||
|
||||
You can use the Minimal Access role to give the same member more than one role in a group:
|
||||
You can use the Minimal Access role with [SAML SSO for GitLab.com groups](group/saml_sso/index.md)
|
||||
to control access to groups and projects in the group hierarchy. You can set the default role to
|
||||
Minimal Access for members automatically added to the root group through SSO.
|
||||
|
||||
1. Add the member to the root group with a Minimal Access role.
|
||||
1. Invite the member as a direct member with a specific role in any subgroup or project in that group.
|
||||
1. On the left sidebar, select **Search or go to** and find your group.
|
||||
1. Select **Settings > SAML SSO**.
|
||||
1. From the **Default membership role** dropdown list, select **Minimal Access**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
### Minimal access users receive 404 errors
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,6 @@ DETAILS:
|
|||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/) in GitLab 17.3.
|
||||
|
||||
You can configure GitLab to send notifications to a Matrix room.
|
||||
To set up the Matrix integration, you must:
|
||||
|
||||
1. [Set up the Matrix integration in GitLab](#set-up-the-matrix-integration-in-gitlab).
|
||||
|
||||
## Set up the Matrix integration in GitLab
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module Type
|
||||
class TagListType < ActiveModel::Type::Value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -112,7 +112,14 @@ module Gitlab
|
|||
path_segments.pop
|
||||
end while path_segments.length >= 2
|
||||
|
||||
Project.where_full_path_in(project_paths).first
|
||||
project = Project.where_full_path_in(project_paths).first
|
||||
return project if project
|
||||
|
||||
# It's possible that the project was transferred and has a redirect
|
||||
redirect = RedirectRoute.for_source_type(Project).by_paths(project_paths).first
|
||||
return redirect.source if redirect
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
# can_read_project? checks if the request's credentials have read access to the project
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ module Gitlab
|
|||
with_redis do |redis|
|
||||
redis.rpush(redis_key, serialize(args, context))
|
||||
end
|
||||
|
||||
deferred_job_counter.increment({ worker: worker_name })
|
||||
end
|
||||
|
||||
def queue_size
|
||||
|
|
@ -96,6 +98,11 @@ module Gitlab
|
|||
def remove_processed_jobs(redis, limit:)
|
||||
redis.ltrim(redis_key, limit, -1)
|
||||
end
|
||||
|
||||
def deferred_job_counter
|
||||
@deferred_job_count ||= ::Gitlab::Metrics.counter(:sidekiq_concurrency_limit_deferred_jobs_total,
|
||||
'Count of jobs deferred by the concurrency limit middleware.')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -53848,6 +53848,9 @@ msgstr ""
|
|||
msgid "There is no data available. Please change your selection."
|
||||
msgstr ""
|
||||
|
||||
msgid "There is no job log to troubleshoot."
|
||||
msgstr ""
|
||||
|
||||
msgid "There is no table data available."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@
|
|||
"swagger-cli": "^4.0.4",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"timezone-mock": "^1.0.8",
|
||||
"vite": "^5.3.4",
|
||||
"vite": "^5.3.5",
|
||||
"vite-plugin-ruby": "^5.0.0",
|
||||
"vue-loader-vue3": "npm:vue-loader@17.4.2",
|
||||
"vue-test-utils-compat": "0.0.14",
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../lib/glfm/update_example_snapshots'
|
||||
Glfm::UpdateExampleSnapshots.new.process(skip_static_and_wysiwyg: ENV['SKIP_STATIC_AND_WYSIWYG'] == 'true')
|
||||
Glfm::UpdateExampleSnapshots.new.process
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
import fs from 'fs';
|
||||
import jsYaml from 'js-yaml';
|
||||
import { renderHtmlAndJsonForAllExamples } from 'jest/content_editor/render_html_and_json_for_all_examples';
|
||||
|
||||
/* eslint-disable no-undef */
|
||||
jest.mock('~/emoji');
|
||||
|
||||
// The purpose of this file is to deserialize markdown examples
|
||||
// to WYSIWYG HTML and to prosemirror documents in JSON form, using
|
||||
// the logic implemented as part of the Content Editor.
|
||||
//
|
||||
// It reads an input YAML file containing all the markdown examples,
|
||||
// and outputs a YAML files containing the rendered HTML and JSON
|
||||
// corresponding each markdown example.
|
||||
//
|
||||
// The input and output file paths are provides as command line arguments.
|
||||
//
|
||||
// Although it is implemented as a Jest test, it is not a unit test. We use
|
||||
// Jest because that is the simplest environment in which to execute the
|
||||
// relevant Content Editor logic.
|
||||
//
|
||||
// This script should be invoked via jest with the a command similar to the following:
|
||||
// yarn jest --testMatch '**/render_wysiwyg_html_and_json.js' ./scripts/lib/glfm/render_wysiwyg_html_and_json.js
|
||||
it('serializes html to prosemirror json', async () => {
|
||||
jest.setTimeout(20000);
|
||||
|
||||
const inputMarkdownTempfilePath = process.env.INPUT_MARKDOWN_YML_PATH;
|
||||
expect(inputMarkdownTempfilePath).not.toBeUndefined();
|
||||
const outputWysiwygHtmlAndJsonTempfilePath =
|
||||
process.env.OUTPUT_WYSIWYG_HTML_AND_JSON_TEMPFILE_PATH;
|
||||
expect(outputWysiwygHtmlAndJsonTempfilePath).not.toBeUndefined();
|
||||
/* eslint-enable no-undef */
|
||||
|
||||
const markdownExamples = jsYaml.safeLoad(fs.readFileSync(inputMarkdownTempfilePath), {});
|
||||
|
||||
const htmlAndJsonExamples = await renderHtmlAndJsonForAllExamples(markdownExamples);
|
||||
|
||||
const htmlAndJsonExamplesYamlString = jsYaml.safeDump(htmlAndJsonExamples, {});
|
||||
fs.writeFileSync(outputWysiwygHtmlAndJsonTempfilePath, htmlAndJsonExamplesYamlString);
|
||||
});
|
||||
|
|
@ -24,10 +24,9 @@ module Glfm
|
|||
include Shared
|
||||
include ParseExamples
|
||||
|
||||
# skip_static_and_wysiwyg can be used to skip the backend/frontend html and prosemirror JSON
|
||||
# generation which depends on external calls. This allows for faster processing in unit tests
|
||||
# which do not require it.
|
||||
def process(skip_static_and_wysiwyg: false)
|
||||
# skip_static can be used to skip the backend html generation which depends on external
|
||||
# calls. This allows for faster processing in unit tests which do not require it.
|
||||
def process(skip_static: false)
|
||||
output('Updating example snapshots...')
|
||||
|
||||
output("Reading #{ES_SNAPSHOT_SPEC_MD_PATH}...")
|
||||
|
|
@ -41,7 +40,7 @@ module Glfm
|
|||
|
||||
reject_disabled_examples(all_examples)
|
||||
|
||||
write_snapshot_example_files(all_examples, skip_static_and_wysiwyg: skip_static_and_wysiwyg)
|
||||
write_snapshot_example_files(all_examples, skip_static: skip_static)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -112,7 +111,7 @@ module Glfm
|
|||
all_examples.reject! { |example| example[:disabled] }
|
||||
end
|
||||
|
||||
def write_snapshot_example_files(all_examples, skip_static_and_wysiwyg:)
|
||||
def write_snapshot_example_files(all_examples, skip_static: false)
|
||||
output("Reading #{GLFM_EXAMPLE_STATUS_YML_PATH}...")
|
||||
glfm_examples_statuses = YAML.safe_load(File.open(GLFM_EXAMPLE_STATUS_YML_PATH), symbolize_names: true) || {}
|
||||
validate_glfm_example_status_yml(glfm_examples_statuses)
|
||||
|
|
@ -123,8 +122,8 @@ module Glfm
|
|||
|
||||
write_markdown_yml(all_examples)
|
||||
|
||||
if skip_static_and_wysiwyg
|
||||
output("Skipping static/WYSIWYG HTML and prosemirror JSON generation...")
|
||||
if skip_static
|
||||
output("Skipping static HTML generation...")
|
||||
return
|
||||
end
|
||||
|
||||
|
|
@ -134,11 +133,8 @@ module Glfm
|
|||
# it straightforward to pass arguments via the command line.
|
||||
ENV['INPUT_MARKDOWN_YML_PATH'], ENV['INPUT_METADATA_YML_PATH'] = copy_tempfiles_for_subprocesses
|
||||
static_html_hash = generate_static_html
|
||||
wysiwyg_html_and_json_hash = generate_wysiwyg_html_and_json
|
||||
|
||||
write_html_yml(all_examples, static_html_hash, wysiwyg_html_and_json_hash, glfm_examples_statuses)
|
||||
|
||||
write_prosemirror_json_yml(all_examples, wysiwyg_html_and_json_hash, glfm_examples_statuses)
|
||||
write_html_yml(all_examples, static_html_hash, glfm_examples_statuses)
|
||||
end
|
||||
|
||||
def validate_glfm_example_status_yml(glfm_examples_statuses)
|
||||
|
|
@ -281,23 +277,7 @@ module Glfm
|
|||
YAML.safe_load(File.open(static_html_tempfile_path), symbolize_names: true)
|
||||
end
|
||||
|
||||
def generate_wysiwyg_html_and_json
|
||||
output("Generating WYSIWYG HTML and prosemirror JSON from markdown examples...")
|
||||
|
||||
# Dir::Tmpname.create requires a block, but we are using the non-block form to get the path
|
||||
# via the return value, so we pass an empty block to avoid an error.
|
||||
wysiwyg_html_and_json_tempfile_path = Dir::Tmpname.create(WYSIWYG_HTML_AND_JSON_TEMPFILE_BASENAME) {}
|
||||
ENV['OUTPUT_WYSIWYG_HTML_AND_JSON_TEMPFILE_PATH'] = wysiwyg_html_and_json_tempfile_path
|
||||
|
||||
cmd = "yarn jest:scripts #{__dir__}/render_wysiwyg_html_and_json.js"
|
||||
run_external_cmd(cmd)
|
||||
|
||||
output("Reading generated WYSIWYG HTML and prosemirror JSON from tempfile " \
|
||||
"#{wysiwyg_html_and_json_tempfile_path}...")
|
||||
YAML.safe_load(File.open(wysiwyg_html_and_json_tempfile_path), symbolize_names: true)
|
||||
end
|
||||
|
||||
def write_html_yml(all_examples, static_html_hash, wysiwyg_html_and_json_hash, glfm_examples_statuses)
|
||||
def write_html_yml(all_examples, static_html_hash, glfm_examples_statuses)
|
||||
generate_and_write_for_all_examples(
|
||||
all_examples, ES_HTML_YML_PATH, glfm_examples_statuses: glfm_examples_statuses
|
||||
) do |example, hash, existing_hash|
|
||||
|
|
@ -310,37 +290,13 @@ module Glfm
|
|||
static_html_hash[name]
|
||||
end
|
||||
|
||||
wysiwyg = if example_statuses[:skip_update_example_snapshot_html_wysiwyg]
|
||||
existing_hash.dig(name, :wysiwyg)
|
||||
else
|
||||
wysiwyg_html_and_json_hash.dig(name, :html)
|
||||
end
|
||||
|
||||
hash[name] = {
|
||||
'canonical' => example.fetch(:html),
|
||||
'static' => static,
|
||||
'wysiwyg' => wysiwyg
|
||||
'static' => static
|
||||
}.compact # Do not assign nil values
|
||||
end
|
||||
end
|
||||
|
||||
def write_prosemirror_json_yml(all_examples, wysiwyg_html_and_json_hash, glfm_examples_statuses)
|
||||
generate_and_write_for_all_examples(
|
||||
all_examples, ES_PROSEMIRROR_JSON_YML_PATH, glfm_examples_statuses: glfm_examples_statuses
|
||||
) do |example, hash, existing_hash|
|
||||
name = example.fetch(:name).to_sym
|
||||
|
||||
json = if glfm_examples_statuses.dig(name, :skip_update_example_snapshot_prosemirror_json)
|
||||
existing_hash[name]
|
||||
else
|
||||
wysiwyg_html_and_json_hash.dig(name, :json)
|
||||
end
|
||||
|
||||
# Do not assign nil values
|
||||
hash[name] = json if json
|
||||
end
|
||||
end
|
||||
|
||||
def generate_and_write_for_all_examples(
|
||||
all_examples, output_file_path, glfm_examples_statuses: {}, literal_scalars: true
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,13 +10,6 @@ RSpec.describe 'admin deploy keys', :js, feature_category: :system_access do
|
|||
let!(:deploy_key) { create(:deploy_key, public: true) }
|
||||
let!(:another_deploy_key) { create(:another_deploy_key, public: true) }
|
||||
|
||||
def page_breadcrumbs
|
||||
all('[data-testid=breadcrumb-links] a').map do |a|
|
||||
# We use `.dom_attribute` because Selenium transforms `.attribute('href')` to include the full URL.
|
||||
{ text: a.text, href: a.native.dom_attribute('href') }
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
enable_admin_mode!(admin)
|
||||
|
|
|
|||
|
|
@ -172,6 +172,17 @@ RSpec.describe 'Admin Groups', feature_category: :groups_and_projects do
|
|||
end
|
||||
|
||||
describe 'group edit' do
|
||||
it 'shows all breadcrumbs', :js do
|
||||
visit admin_group_edit_path(group)
|
||||
|
||||
expect(page_breadcrumbs).to eq([
|
||||
{ text: 'Admin area', href: admin_root_path },
|
||||
{ text: 'Groups', href: admin_groups_path },
|
||||
{ text: group.name, href: admin_group_path(group) },
|
||||
{ text: 'Edit', href: admin_group_edit_path(group) }
|
||||
])
|
||||
end
|
||||
|
||||
it 'shows the visibility level radio populated with the group visibility_level value' do
|
||||
group = create(:group, :private)
|
||||
|
||||
|
|
|
|||
|
|
@ -176,6 +176,18 @@ RSpec.describe "Admin::Projects", feature_category: :groups_and_projects do
|
|||
end
|
||||
|
||||
describe 'project edit' do
|
||||
it 'shows all breadcrumbs', :js do
|
||||
project_params = { id: project.to_param, namespace_id: project.namespace.to_param }
|
||||
visit edit_admin_namespace_project_path(project_params)
|
||||
|
||||
expect(page_breadcrumbs).to eq([
|
||||
{ text: 'Admin area', href: admin_root_path },
|
||||
{ text: 'Projects', href: admin_projects_path },
|
||||
{ text: project.full_name, href: admin_namespace_project_path(project_params) },
|
||||
{ text: 'Edit', href: edit_admin_namespace_project_path(project_params) }
|
||||
])
|
||||
end
|
||||
|
||||
it 'updates project details' do
|
||||
project = create(:project, :private, name: 'Garfield', description: 'Funny Cat')
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Admin Topics', feature_category: :groups_and_projects do
|
||||
let_it_be(:topic) { create :topic }
|
||||
let_it_be(:admin) { create(:admin) }
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
enable_admin_mode!(admin)
|
||||
end
|
||||
|
||||
describe 'topic edit' do
|
||||
it 'shows all breadcrumbs', :js do
|
||||
visit edit_admin_topic_path(topic)
|
||||
|
||||
expect(page_breadcrumbs).to eq([
|
||||
{ text: 'Admin area', href: admin_root_path },
|
||||
{ text: 'Topics', href: admin_topics_path },
|
||||
{ text: topic.name, href: edit_admin_topic_path(topic) },
|
||||
{ text: 'Edit', href: edit_admin_topic_path(topic) }
|
||||
])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -565,6 +565,15 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do
|
|||
visit edit_admin_user_path(user)
|
||||
end
|
||||
|
||||
it 'shows all breadcrumbs', :js do
|
||||
expect(page_breadcrumbs).to eq([
|
||||
{ text: 'Admin area', href: admin_root_path },
|
||||
{ text: 'Users', href: admin_users_path },
|
||||
{ text: user.name, href: admin_user_path(user) },
|
||||
{ text: 'Edit', href: edit_admin_user_path(user) }
|
||||
])
|
||||
end
|
||||
|
||||
describe 'Update user' do
|
||||
before do
|
||||
fill_in 'user_name', with: 'Big Bang'
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
// See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing
|
||||
// for documentation on this spec.
|
||||
//
|
||||
// NOTE: Unlike the backend markdown_snapshot_spec.rb which has a CE and EE version, there is only
|
||||
// one version of this spec. This is because the frontend markdown rendering does not require EE-only
|
||||
// backend features.
|
||||
|
||||
import jsYaml from 'js-yaml';
|
||||
import { pick } from 'lodash';
|
||||
import glfmExampleStatusYml from '../../../glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml';
|
||||
import markdownYml from '../../../glfm_specification/output_example_snapshots/markdown.yml';
|
||||
import htmlYml from '../../../glfm_specification/output_example_snapshots/html.yml';
|
||||
import prosemirrorJsonYml from '../../../glfm_specification/output_example_snapshots/prosemirror_json.yml';
|
||||
import {
|
||||
IMPLEMENTATION_ERROR_MSG,
|
||||
renderHtmlAndJsonForAllExamples,
|
||||
} from './render_html_and_json_for_all_examples';
|
||||
|
||||
jest.mock('~/emoji');
|
||||
|
||||
const filterExamples = (examples) => {
|
||||
const focusedMarkdownExamples = process.env.FOCUSED_MARKDOWN_EXAMPLES?.split(',') || [];
|
||||
if (!focusedMarkdownExamples.length) {
|
||||
return examples;
|
||||
}
|
||||
return pick(examples, focusedMarkdownExamples);
|
||||
};
|
||||
|
||||
const loadExamples = (yaml) => {
|
||||
const examples = jsYaml.safeLoad(yaml, {});
|
||||
return filterExamples(examples);
|
||||
};
|
||||
|
||||
describe('markdown example snapshots in ContentEditor', () => {
|
||||
let actualHtmlAndJsonExamples;
|
||||
let skipRunningSnapshotWysiwygHtmlTests;
|
||||
let skipRunningSnapshotProsemirrorJsonTests;
|
||||
|
||||
const exampleStatuses = loadExamples(glfmExampleStatusYml);
|
||||
const markdownExamples = loadExamples(markdownYml);
|
||||
const expectedHtmlExamples = loadExamples(htmlYml);
|
||||
const expectedProseMirrorJsonExamples = loadExamples(prosemirrorJsonYml);
|
||||
const exampleNames = Object.keys(markdownExamples);
|
||||
|
||||
beforeAll(() => {
|
||||
return renderHtmlAndJsonForAllExamples(markdownExamples).then((examples) => {
|
||||
actualHtmlAndJsonExamples = examples;
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(exampleNames)('%s', (name) => {
|
||||
const exampleNamePrefix = 'verifies conversion of GLFM to';
|
||||
skipRunningSnapshotWysiwygHtmlTests =
|
||||
exampleStatuses[name]?.skip_running_snapshot_wysiwyg_html_tests;
|
||||
skipRunningSnapshotProsemirrorJsonTests =
|
||||
exampleStatuses[name]?.skip_running_snapshot_prosemirror_json_tests;
|
||||
|
||||
const markdown = markdownExamples[name];
|
||||
|
||||
if (skipRunningSnapshotWysiwygHtmlTests) {
|
||||
it.todo(`${exampleNamePrefix} HTML: ${skipRunningSnapshotWysiwygHtmlTests}`);
|
||||
} else {
|
||||
it(`${exampleNamePrefix} HTML`, () => {
|
||||
const expectedHtml = expectedHtmlExamples[name].wysiwyg;
|
||||
const { html: actualHtml } = actualHtmlAndJsonExamples[name];
|
||||
|
||||
expect(actualHtml).toMatchExpectedForMarkdown(
|
||||
'HTML',
|
||||
name,
|
||||
markdown,
|
||||
IMPLEMENTATION_ERROR_MSG,
|
||||
expectedHtml,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (skipRunningSnapshotProsemirrorJsonTests) {
|
||||
it.todo(`${exampleNamePrefix} ProseMirror JSON: ${skipRunningSnapshotProsemirrorJsonTests}`);
|
||||
} else {
|
||||
it(`${exampleNamePrefix} ProseMirror JSON`, () => {
|
||||
const expectedJson = expectedProseMirrorJsonExamples[name];
|
||||
const { json: actualJson } = actualHtmlAndJsonExamples[name];
|
||||
|
||||
expect(actualJson).toMatchExpectedForMarkdown(
|
||||
'JSON',
|
||||
name,
|
||||
markdown,
|
||||
IMPLEMENTATION_ERROR_MSG,
|
||||
expectedJson,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import { DOMSerializer } from '@tiptap/pm/model';
|
||||
import createMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
|
||||
import { createTiptapEditor } from 'jest/content_editor/test_utils';
|
||||
|
||||
const tiptapEditor = createTiptapEditor();
|
||||
|
||||
export const IMPLEMENTATION_ERROR_MSG = 'Error - check implementation';
|
||||
|
||||
async function renderMarkdownToHTMLAndJSON(markdown, schema, deserializer) {
|
||||
let prosemirrorDocument;
|
||||
try {
|
||||
const { document } = await deserializer.deserialize({ schema, markdown });
|
||||
prosemirrorDocument = document;
|
||||
} catch (e) {
|
||||
const errorMsg = `${IMPLEMENTATION_ERROR_MSG}:\n${e.message}`;
|
||||
return {
|
||||
html: errorMsg,
|
||||
json: errorMsg,
|
||||
};
|
||||
}
|
||||
|
||||
const documentFragment = DOMSerializer.fromSchema(schema).serializeFragment(
|
||||
prosemirrorDocument.content,
|
||||
);
|
||||
const htmlString = Array.from(documentFragment.children)
|
||||
.map((el) => el.outerHTML)
|
||||
.join('\n');
|
||||
|
||||
const json = prosemirrorDocument.toJSON();
|
||||
const jsonString = JSON.stringify(json, null, 2);
|
||||
return { html: htmlString, json: jsonString };
|
||||
}
|
||||
|
||||
export function renderHtmlAndJsonForAllExamples(markdownExamples) {
|
||||
const { schema } = tiptapEditor;
|
||||
const deserializer = createMarkdownDeserializer();
|
||||
const exampleNames = Object.keys(markdownExamples);
|
||||
|
||||
return exampleNames.reduce(async (promisedExamples, exampleName) => {
|
||||
const markdown = markdownExamples[exampleName];
|
||||
const htmlAndJson = await renderMarkdownToHTMLAndJSON(markdown, schema, deserializer);
|
||||
const examples = await promisedExamples;
|
||||
examples[exampleName] = htmlAndJson;
|
||||
return examples;
|
||||
}, Promise.resolve({}));
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ describe('IssuableDescription', () => {
|
|||
});
|
||||
|
||||
it('renders hidden textarea element when issuable.description is present and enableTaskList prop is true', () => {
|
||||
const textareaEl = wrapper.find('textarea.gl-display-none.js-task-list-field');
|
||||
const textareaEl = wrapper.find('textarea.gl-hidden.js-task-list-field');
|
||||
|
||||
expect(textareaEl.exists()).toBe(true);
|
||||
expect(textareaEl.attributes('data-update-url')).toBe(`${mockIssuable.webUrl}.json`);
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ describe('IssuableTitle', () => {
|
|||
|
||||
expect(titleEl.exists()).toBe(true);
|
||||
expect(titleEl.html()).toBe(
|
||||
'<h1 dir="auto" data-testid="issuable-title" class="title gl-font-size-h-display"><b>Sample</b> title</h1>',
|
||||
'<h1 dir="auto" data-testid="issuable-title" class="title gl-text-size-h-display"><b>Sample</b> title</h1>',
|
||||
);
|
||||
|
||||
wrapperWithTitle.destroy();
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ RSpec.describe Mutations::Achievements::Award, feature_category: :user_profile d
|
|||
|
||||
describe '#resolve' do
|
||||
subject(:resolve_mutation) do
|
||||
described_class.new(object: nil, context: { current_user: current_user }, field: nil).resolve(
|
||||
described_class.new(object: nil, context: query_context, field: nil).resolve(
|
||||
achievement_id: achievement&.to_global_id, user_id: recipient&.to_global_id
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe Mutations::Achievements::Create, feature_category: :user_profile do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
|
||||
let(:group) { create(:group) }
|
||||
let(:valid_params) do
|
||||
|
|
@ -14,7 +14,7 @@ RSpec.describe Mutations::Achievements::Create, feature_category: :user_profile
|
|||
|
||||
describe '#resolve' do
|
||||
subject(:resolve_mutation) do
|
||||
described_class.new(object: nil, context: { current_user: user }, field: nil).resolve(
|
||||
described_class.new(object: nil, context: query_context, field: nil).resolve(
|
||||
**valid_params,
|
||||
namespace_id: group.to_global_id
|
||||
)
|
||||
|
|
@ -22,7 +22,7 @@ RSpec.describe Mutations::Achievements::Create, feature_category: :user_profile
|
|||
|
||||
context 'when the user does not have permission' do
|
||||
before do
|
||||
group.add_developer(user)
|
||||
group.add_developer(current_user)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
|
|
@ -33,7 +33,7 @@ RSpec.describe Mutations::Achievements::Create, feature_category: :user_profile
|
|||
|
||||
context 'when the user has permission' do
|
||||
before do
|
||||
group.add_maintainer(user)
|
||||
group.add_maintainer(current_user)
|
||||
end
|
||||
|
||||
context 'when the params are invalid' do
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ RSpec.describe Mutations::Achievements::Delete, feature_category: :user_profile
|
|||
|
||||
describe '#resolve' do
|
||||
subject(:resolve_mutation) do
|
||||
described_class.new(object: nil, context: { current_user: current_user }, field: nil).resolve(
|
||||
described_class.new(object: nil, context: query_context, field: nil).resolve(
|
||||
achievement_id: achievement&.to_global_id
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ RSpec.describe Mutations::Achievements::DeleteUserAchievement, feature_category:
|
|||
|
||||
describe '#resolve' do
|
||||
subject(:resolve_mutation) do
|
||||
described_class.new(object: nil, context: { current_user: current_user }, field: nil).resolve(
|
||||
described_class.new(object: nil, context: query_context, field: nil).resolve(
|
||||
user_achievement_id: user_achievement&.to_global_id
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ RSpec.describe Mutations::Achievements::Revoke, feature_category: :user_profile
|
|||
|
||||
describe '#resolve' do
|
||||
subject(:resolve_mutation) do
|
||||
described_class.new(object: nil, context: { current_user: current_user }, field: nil).resolve(
|
||||
described_class.new(object: nil, context: query_context, field: nil).resolve(
|
||||
user_achievement_id: user_achievement&.to_global_id
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ RSpec.describe Mutations::Achievements::Update, feature_category: :user_profile
|
|||
|
||||
describe '#resolve' do
|
||||
subject(:resolve_mutation) do
|
||||
described_class.new(object: nil, context: { current_user: current_user }, field: nil).resolve(
|
||||
described_class.new(object: nil, context: query_context, field: nil).resolve(
|
||||
achievement_id: achievement&.to_global_id, name: name
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::AlertManagement::Alerts::SetAssignees do
|
||||
RSpec.describe Mutations::AlertManagement::Alerts::SetAssignees, feature_category: :api do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:starting_assignee) { create(:user) }
|
||||
let_it_be(:unassigned_user) { create(:user) }
|
||||
let_it_be(:alert) { create(:alert_management_alert, assignees: [starting_assignee]) }
|
||||
|
|
@ -170,7 +172,7 @@ RSpec.describe Mutations::AlertManagement::Alerts::SetAssignees do
|
|||
end
|
||||
end
|
||||
|
||||
def mutation_for(project, user)
|
||||
described_class.new(object: project, context: { current_user: user }, field: nil)
|
||||
def mutation_for(project, _user)
|
||||
described_class.new(object: project, context: query_context, field: nil)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,18 +2,19 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::AlertManagement::Alerts::Todo::Create do
|
||||
subject(:mutation) { described_class.new(object: project, context: { current_user: current_user }, field: nil) }
|
||||
RSpec.describe Mutations::AlertManagement::Alerts::Todo::Create, feature_category: :api do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:alert) { create(:alert_management_alert) }
|
||||
let_it_be(:project) { alert.project }
|
||||
|
||||
let(:current_user) { project.first_owner }
|
||||
|
||||
let(:args) { { project_path: project.full_path, iid: alert.iid } }
|
||||
|
||||
specify { expect(described_class).to require_graphql_authorizations(:update_alert_management_alert) }
|
||||
|
||||
subject(:mutation) { described_class.new(object: project, context: query_context, field: nil) }
|
||||
|
||||
describe '#resolve' do
|
||||
subject(:resolve) { mutation.resolve(args) }
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::AlertManagement::CreateAlertIssue do
|
||||
RSpec.describe Mutations::AlertManagement::CreateAlertIssue, feature_category: :api do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:alert) { create(:alert_management_alert, project: project, status: 'triggered') }
|
||||
|
|
@ -74,7 +76,7 @@ RSpec.describe Mutations::AlertManagement::CreateAlertIssue do
|
|||
|
||||
private
|
||||
|
||||
def mutation_for(project, user)
|
||||
described_class.new(object: project, context: { current_user: user }, field: nil)
|
||||
def mutation_for(project, _user)
|
||||
described_class.new(object: project, context: query_context, field: nil)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::AlertManagement::HttpIntegration::Create do
|
||||
RSpec.describe Mutations::AlertManagement::HttpIntegration::Create, feature_category: :api do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
let(:args) { { project_path: project.full_path, active: true, name: 'HTTP Integration' } }
|
||||
|
||||
specify { expect(described_class).to require_graphql_authorizations(:admin_operations) }
|
||||
|
|
@ -52,7 +53,7 @@ RSpec.describe Mutations::AlertManagement::HttpIntegration::Create do
|
|||
|
||||
private
|
||||
|
||||
def mutation_for(project, user)
|
||||
described_class.new(object: project, context: { current_user: user }, field: nil)
|
||||
def mutation_for(project, _user)
|
||||
described_class.new(object: project, context: query_context, field: nil)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::AlertManagement::HttpIntegration::Destroy do
|
||||
RSpec.describe Mutations::AlertManagement::HttpIntegration::Destroy, feature_category: :api do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
|
|
@ -53,7 +55,7 @@ RSpec.describe Mutations::AlertManagement::HttpIntegration::Destroy do
|
|||
|
||||
private
|
||||
|
||||
def mutation_for(project, user)
|
||||
described_class.new(object: project, context: { current_user: user }, field: nil)
|
||||
def mutation_for(project, _user)
|
||||
described_class.new(object: project, context: query_context, field: nil)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::AlertManagement::HttpIntegration::ResetToken do
|
||||
RSpec.describe Mutations::AlertManagement::HttpIntegration::ResetToken, feature_category: :api do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:integration) { create(:alert_management_http_integration, project: project) }
|
||||
|
|
@ -53,7 +55,7 @@ RSpec.describe Mutations::AlertManagement::HttpIntegration::ResetToken do
|
|||
|
||||
private
|
||||
|
||||
def mutation_for(project, user)
|
||||
described_class.new(object: project, context: { current_user: user }, field: nil)
|
||||
def mutation_for(project, _user)
|
||||
described_class.new(object: project, context: query_context, field: nil)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::AlertManagement::HttpIntegration::Update do
|
||||
RSpec.describe Mutations::AlertManagement::HttpIntegration::Update, feature_category: :api do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:integration) { create(:alert_management_http_integration, project: project) }
|
||||
|
|
@ -53,7 +55,7 @@ RSpec.describe Mutations::AlertManagement::HttpIntegration::Update do
|
|||
|
||||
private
|
||||
|
||||
def mutation_for(project, user)
|
||||
described_class.new(object: project, context: { current_user: user }, field: nil)
|
||||
def mutation_for(project, _user)
|
||||
described_class.new(object: project, context: query_context, field: nil)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Create do
|
||||
RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Create, feature_category: :api do
|
||||
include GraphqlHelpers
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
|
|
@ -76,7 +77,7 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Create do
|
|||
|
||||
private
|
||||
|
||||
def mutation_for(project, user)
|
||||
described_class.new(object: project, context: { current_user: user }, field: nil)
|
||||
def mutation_for(project, _user)
|
||||
described_class.new(object: project, context: query_context, field: nil)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::AlertManagement::PrometheusIntegration::ResetToken do
|
||||
RSpec.describe Mutations::AlertManagement::PrometheusIntegration::ResetToken, feature_category: :api do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:integration) { create(:prometheus_integration, project: project) }
|
||||
|
|
@ -53,7 +55,7 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::ResetToken do
|
|||
|
||||
private
|
||||
|
||||
def mutation_for(project, user)
|
||||
described_class.new(object: project, context: { current_user: user }, field: nil)
|
||||
def mutation_for(project, _user)
|
||||
described_class.new(object: project, context: query_context, field: nil)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Update do
|
||||
RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Update, feature_category: :api do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:integration) { create(:prometheus_integration, project: project) }
|
||||
|
|
@ -53,7 +55,7 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Update do
|
|||
|
||||
private
|
||||
|
||||
def mutation_for(project, user)
|
||||
described_class.new(object: project, context: { current_user: user }, field: nil)
|
||||
def mutation_for(project, _user)
|
||||
described_class.new(object: project, context: query_context, field: nil)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::AlertManagement::UpdateAlertStatus do
|
||||
RSpec.describe Mutations::AlertManagement::UpdateAlertStatus, feature_category: :api do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:alert) { create(:alert_management_alert, :triggered) }
|
||||
let_it_be(:project) { alert.project }
|
||||
|
|
@ -80,7 +82,7 @@ RSpec.describe Mutations::AlertManagement::UpdateAlertStatus do
|
|||
|
||||
private
|
||||
|
||||
def mutation_for(project, user)
|
||||
described_class.new(object: project, context: { current_user: user }, field: nil)
|
||||
def mutation_for(project, _user)
|
||||
described_class.new(object: project, context: query_context, field: nil)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Boards::Update do
|
||||
RSpec.describe Mutations::Boards::Update, feature_category: :api do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:board) { create(:board, project: project) }
|
||||
|
||||
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
|
||||
let(:mutation) { described_class.new(object: nil, context: query_context, field: nil) }
|
||||
let(:mutated_board) { subject[:board] }
|
||||
|
||||
let(:mutation_params) do
|
||||
|
|
@ -31,7 +33,7 @@ RSpec.describe Mutations::Boards::Update do
|
|||
|
||||
context 'when user can update board' do
|
||||
before do
|
||||
board.resource_parent.add_reporter(user)
|
||||
board.resource_parent.add_reporter(current_user)
|
||||
end
|
||||
|
||||
it 'updates board with correct values' do
|
||||
|
|
|
|||
|
|
@ -2,20 +2,13 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Branches::Create do
|
||||
RSpec.describe Mutations::Branches::Create, feature_category: :api do
|
||||
include GraphqlHelpers
|
||||
|
||||
subject(:mutation) { described_class.new(object: nil, context: context, field: nil) }
|
||||
|
||||
let_it_be(:project) { create(:project, :public, :repository) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
|
||||
let(:context) do
|
||||
GraphQL::Query::Context.new(
|
||||
query: query_double(schema: nil),
|
||||
values: { current_user: user }
|
||||
)
|
||||
end
|
||||
subject(:mutation) { described_class.new(object: nil, context: query_context, field: nil) }
|
||||
|
||||
describe '#resolve' do
|
||||
subject { mutation.resolve(project_path: project.full_path, name: branch, ref: ref) }
|
||||
|
|
@ -30,9 +23,9 @@ RSpec.describe Mutations::Branches::Create do
|
|||
|
||||
context 'when the user can create a branch' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
project.add_developer(current_user)
|
||||
|
||||
allow_next_instance_of(::Branches::CreateService, project, user) do |create_service|
|
||||
allow_next_instance_of(::Branches::CreateService, project, current_user) do |create_service|
|
||||
allow(create_service).to receive(:execute).with(branch, ref) { service_result }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Ci::JobTokenScope::AddGroupOrProject, feature_category: :continuous_integration do
|
||||
include GraphqlHelpers
|
||||
|
||||
let(:mutation) do
|
||||
described_class.new(object: nil, context: { current_user: current_user }, field: nil)
|
||||
described_class.new(object: nil, context: query_context, field: nil)
|
||||
end
|
||||
|
||||
describe '#resolve' do
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Ci::JobTokenScope::AddProject, feature_category: :continuous_integration do
|
||||
include GraphqlHelpers
|
||||
|
||||
let(:mutation) do
|
||||
described_class.new(object: nil, context: { current_user: current_user }, field: nil)
|
||||
described_class.new(object: nil, context: query_context, field: nil)
|
||||
end
|
||||
|
||||
describe '#resolve' do
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Ci::JobTokenScope::RemoveGroup, feature_category: :continuous_integration do
|
||||
include GraphqlHelpers
|
||||
|
||||
let(:mutation) do
|
||||
described_class.new(object: nil, context: { current_user: current_user }, field: nil)
|
||||
described_class.new(object: nil, context: query_context, field: nil)
|
||||
end
|
||||
|
||||
describe '#resolve' do
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Ci::JobTokenScope::RemoveProject, feature_category: :continuous_integration do
|
||||
include GraphqlHelpers
|
||||
|
||||
let(:mutation) do
|
||||
described_class.new(object: nil, context: { current_user: current_user }, field: nil)
|
||||
described_class.new(object: nil, context: query_context, field: nil)
|
||||
end
|
||||
|
||||
describe '#resolve' do
|
||||
|
|
|
|||
|
|
@ -2,26 +2,27 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::DesignManagement::Delete do
|
||||
RSpec.describe Mutations::DesignManagement::Delete, feature_category: :api do
|
||||
include DesignManagementTestHelpers
|
||||
include GraphqlHelpers
|
||||
|
||||
let(:issue) { create(:issue) }
|
||||
let(:current_designs) { issue.designs.current }
|
||||
let(:user) { issue.author }
|
||||
let(:current_user) { issue.author }
|
||||
let(:project) { issue.project }
|
||||
let(:design_a) { create(:design, :with_file, issue: issue) }
|
||||
let(:design_b) { create(:design, :with_file, issue: issue) }
|
||||
let(:design_c) { create(:design, :with_file, issue: issue) }
|
||||
let(:filenames) { [design_a, design_b, design_c].map(&:filename) }
|
||||
|
||||
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
|
||||
let(:mutation) { described_class.new(object: nil, context: context, field: nil) }
|
||||
|
||||
before do
|
||||
stub_const('Errors', Gitlab::Graphql::Errors, transfer_nested_constants: true)
|
||||
end
|
||||
|
||||
def run_mutation
|
||||
mutation = described_class.new(object: nil, context: { current_user: user }, field: nil)
|
||||
mutation = described_class.new(object: nil, context: query_context, field: nil)
|
||||
mutation.resolve(project_path: project.full_path, iid: issue.iid, filenames: filenames)
|
||||
end
|
||||
|
||||
|
|
@ -54,7 +55,7 @@ RSpec.describe Mutations::DesignManagement::Delete do
|
|||
end
|
||||
|
||||
context "when the user is not allowed to delete designs" do
|
||||
let(:user) { create(:user) }
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
it_behaves_like "resource not available"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,16 +2,17 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::DesignManagement::Move do
|
||||
RSpec.describe Mutations::DesignManagement::Move, feature_category: :api do
|
||||
include DesignManagementTestHelpers
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:issue) { create(:issue) }
|
||||
let_it_be(:designs) { create_list(:design, 3, issue: issue) }
|
||||
let_it_be(:developer) { create(:user, developer_of: issue.project) }
|
||||
|
||||
let(:user) { developer }
|
||||
let(:current_user) { developer }
|
||||
|
||||
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
|
||||
let(:mutation) { described_class.new(object: nil, context: query_context, field: nil) }
|
||||
|
||||
let(:current_design) { designs.first }
|
||||
let(:previous_design) { designs.second }
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::DesignManagement::Upload do
|
||||
RSpec.describe Mutations::DesignManagement::Upload, feature_category: :api do
|
||||
include DesignManagementTestHelpers
|
||||
include ConcurrentHelpers
|
||||
include GraphqlHelpers
|
||||
|
||||
let(:issue) { create(:issue) }
|
||||
let(:user) { issue.author }
|
||||
let(:current_user) { issue.author }
|
||||
let(:project) { issue.project }
|
||||
|
||||
subject(:mutation) do
|
||||
described_class.new(object: nil, context: { current_user: user }, field: nil)
|
||||
described_class.new(object: nil, context: query_context, field: nil)
|
||||
end
|
||||
|
||||
def run_mutation(files_to_upload = files, project_path = project.full_path, iid = issue.iid)
|
||||
mutation = described_class.new(object: nil, context: { current_user: user }, field: nil)
|
||||
mutation = described_class.new(object: nil, context: query_context, field: nil)
|
||||
Gitlab::ExclusiveLease.skipping_transaction_check do
|
||||
mutation.resolve(project_path: project_path, iid: iid, files: files_to_upload)
|
||||
end
|
||||
|
|
@ -87,8 +88,8 @@ RSpec.describe Mutations::DesignManagement::Upload do
|
|||
describe 'running requests in parallel on different issues' do
|
||||
it 'does not cause errors' do
|
||||
creates_designs do
|
||||
issues = create_list(:issue, files.size, author: user)
|
||||
issues.each { |i| i.project.add_developer(user) }
|
||||
issues = create_list(:issue, files.size, author: current_user)
|
||||
issues.each { |i| i.project.add_developer(current_user) }
|
||||
blocks = files.zip(issues).map do |(f, i)|
|
||||
-> { run_mutation([f], i.project.full_path, i.iid) }
|
||||
end
|
||||
|
|
@ -110,7 +111,7 @@ RSpec.describe Mutations::DesignManagement::Upload do
|
|||
end
|
||||
|
||||
context "when the user is not allowed to upload designs" do
|
||||
let(:user) { create(:user) }
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
it_behaves_like "resource not available"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -869,12 +869,6 @@ RSpec.describe ApplicationHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'stylesheet_link_tag_defer' do
|
||||
it 'uses media="all" in stylesheet' do
|
||||
expect(helper.stylesheet_link_tag_defer('test')).to eq('<link rel="stylesheet" href="/stylesheets/test.css" media="all" />')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'sign_in_with_redirect?' do
|
||||
context 'when on the sign-in page that redirects afterwards' do
|
||||
before do
|
||||
|
|
|
|||
|
|
@ -71,6 +71,15 @@ RSpec.describe Gitlab::Middleware::Go, feature_category: :source_code_management
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project accessed by a redirect' do
|
||||
let!(:redirect_route) { create(:redirect_route, source: project, path: 'redirect/project') }
|
||||
let(:path) { redirect_route.path }
|
||||
|
||||
it 'returns the full project path' do
|
||||
expect_response_with_path(go, enabled_protocol, project.full_path, project.default_branch)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a nested project path' do
|
||||
|
|
|
|||
|
|
@ -45,6 +45,15 @@ RSpec.describe Gitlab::SidekiqMiddleware::ConcurrencyLimit::ConcurrencyLimitServ
|
|||
|
||||
add_to_queue!
|
||||
end
|
||||
|
||||
it 'reports prometheus metrics' do
|
||||
deferred_job_count_double = instance_double(Prometheus::Client::Counter)
|
||||
expect(Gitlab::Metrics).to receive(:counter).with(:sidekiq_concurrency_limit_deferred_jobs_total, anything)
|
||||
.and_return(deferred_job_count_double)
|
||||
expect(deferred_job_count_double).to receive(:increment).with({ worker: worker_class_name })
|
||||
|
||||
add_to_queue!
|
||||
end
|
||||
end
|
||||
|
||||
describe '.has_jobs_in_queue?' do
|
||||
|
|
|
|||
|
|
@ -48,6 +48,18 @@ RSpec.describe FixSequenceOwnersForCiBuilds, :migration, feature_category: :cont
|
|||
|
||||
ApplicationRecord.connection.execute('DROP SEQUENCE unrelated_seq;')
|
||||
end
|
||||
|
||||
it 'ignores unknown sequences', :aggregate_failures do
|
||||
ApplicationRecord.connection.execute('DROP SEQUENCE IF EXISTS unknown_seq;')
|
||||
ApplicationRecord.connection.execute('CREATE SEQUENCE unknown_seq;')
|
||||
ApplicationRecord.connection.execute(<<-SQL)
|
||||
ALTER SEQUENCE unknown_seq OWNED BY ci_builds.id;
|
||||
SQL
|
||||
|
||||
expect { migration.up }.not_to change { sequence_owner('unknown_seq') }
|
||||
|
||||
ApplicationRecord.connection.execute('DROP SEQUENCE unknown_seq;')
|
||||
end
|
||||
end
|
||||
|
||||
def sequence_owner(sequence_name)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Ci::Taggable, feature_category: :continuous_integration do
|
||||
let_it_be(:taggable_model) do
|
||||
Class.new(Ci::ApplicationRecord) do |_model|
|
||||
connection.create_table :_test_gitlab_ci_taggings, force: true do |t|
|
||||
t.string :name
|
||||
end
|
||||
|
||||
self.table_name = '_test_gitlab_ci_taggings'
|
||||
|
||||
def self.name
|
||||
'TestCiTaggings'
|
||||
end
|
||||
|
||||
include Ci::Taggable
|
||||
end
|
||||
end
|
||||
|
||||
let(:taggable_record) { taggable_model.new(name: 'tags') }
|
||||
|
||||
it { expect(taggable_record).to have_many(:taggings).class_name('Ci::Tagging') }
|
||||
it { expect(taggable_record).to have_many(:tag_taggings).class_name('Ci::Tagging') }
|
||||
it { expect(taggable_record).to have_many(:tags).class_name('Ci::Tag').through(:tag_taggings) }
|
||||
it { expect(taggable_record).to have_many(:base_tags).class_name('Ci::Tag').through(:taggings) }
|
||||
|
||||
it { expect(taggable_record.tag_list).to be_empty }
|
||||
it { expect(taggable_record.tag_list).to be_a(Gitlab::Ci::Tags::TagList) }
|
||||
|
||||
it 'sets the tag list' do
|
||||
taggable_record.tag_list = 'ruby, docker, postgres'
|
||||
|
||||
expect(taggable_record.tag_list).to match_array(%w[ruby docker postgres])
|
||||
expect(taggable_record.tag_list).to be_a(Gitlab::Ci::Tags::TagList)
|
||||
end
|
||||
|
||||
it 'persists the tag list' do
|
||||
taggable_record.tag_list = 'ruby, docker, postgres'
|
||||
|
||||
expect { taggable_record.save! }.to change { Ci::Tag.count }.by(3)
|
||||
end
|
||||
|
||||
it 'loads the tag list' do
|
||||
taggable_record.tag_list = 'ruby, docker, postgres'
|
||||
taggable_record.save!
|
||||
fresh_record = taggable_model.find(taggable_record.id)
|
||||
|
||||
expect(fresh_record.tag_list).to match_array(%w[ruby docker postgres])
|
||||
expect(fresh_record.tags).to be_all(Ci::Tag)
|
||||
end
|
||||
|
||||
it 'removes unwanted tags from the list' do
|
||||
taggable_record.tag_list = 'ruby, docker, postgres'
|
||||
taggable_record.save!
|
||||
|
||||
taggable_record.tag_list = 'ruby, docker'
|
||||
expect { taggable_record.save! }.to change { Ci::Tagging.count }.by(-1)
|
||||
expect(taggable_record.reload.tag_list).to match_array(%w[ruby docker])
|
||||
end
|
||||
|
||||
it 'updates the tag list' do
|
||||
taggable_record.tag_list = 'ruby, docker, postgres'
|
||||
taggable_record.save!
|
||||
|
||||
taggable_record.tag_list = 'ruby, docker, elasticsearch, golang'
|
||||
|
||||
expect { taggable_record.save! }
|
||||
.to change { Ci::Tag.count }.by(2)
|
||||
.and change { Ci::Tagging.count }.by(1)
|
||||
|
||||
expect(taggable_record.reload.tag_list).to match_array(%w[ruby docker elasticsearch golang])
|
||||
end
|
||||
|
||||
describe '.tagged_with' do
|
||||
let_it_be(:tags) { taggable_model.create!(name: 'tags', tag_list: 'ruby, docker, postgres') }
|
||||
let_it_be(:other_tags) { taggable_model.create!(name: 'other tags', tag_list: 'ruby, golang') }
|
||||
|
||||
it { expect(taggable_model.tagged_with('ruby')).to match_array([tags, other_tags]) }
|
||||
it { expect(taggable_model.tagged_with('ruby, docker')).to match_array([tags]) }
|
||||
it { expect(taggable_model.tagged_with(%w[ruby docker])).to match_array([tags]) }
|
||||
it { expect(taggable_model.tagged_with(%w[ruby docker golang])).to be_empty }
|
||||
end
|
||||
|
||||
describe '#reload' do
|
||||
before do
|
||||
taggable_record.tag_list = 'ruby, docker, postgres'
|
||||
taggable_record.save!
|
||||
end
|
||||
|
||||
it { expect { taggable_record.reload }.to change { taggable_record.tag_list.object_id } }
|
||||
end
|
||||
|
||||
describe '#dirtify_tag_list' do
|
||||
before do
|
||||
taggable_record.tag_list = 'ruby, docker, postgres'
|
||||
taggable_record.save!
|
||||
end
|
||||
|
||||
it 'resets the tag list after a tag is added' do
|
||||
expect { taggable_record.tags << create(:ci_tag) }
|
||||
.to change { Ci::Tagging.count }.by(1)
|
||||
.and change { taggable_record.tag_list.size }.by(1)
|
||||
.and change { taggable_record.tag_list_changed? }.to(true)
|
||||
end
|
||||
|
||||
it 'resets the tag list after a tag is removed' do
|
||||
expect { taggable_record.tags.destroy(Ci::Tag.find_by_name('docker')) }
|
||||
.to change { Ci::Tagging.count }.by(-1)
|
||||
.and change { taggable_record.tag_list.size }.by(-1)
|
||||
.and change { taggable_record.tag_list_changed? }.to(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -16,6 +16,46 @@ RSpec.describe RedirectRoute do
|
|||
it { is_expected.to validate_uniqueness_of(:path).case_insensitive }
|
||||
end
|
||||
|
||||
describe '.for_source_type' do
|
||||
subject { described_class.for_source_type(source_type) }
|
||||
|
||||
context 'when Project' do
|
||||
let(:source_type) { Project }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'when Namespace' do
|
||||
let(:source_type) { Namespace }
|
||||
|
||||
it { is_expected.to match_array(redirect_route) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.by_paths' do
|
||||
subject { described_class.by_paths(paths) }
|
||||
|
||||
let!(:redirect2) { group.redirect_routes.create!(path: 'gitlabb/test') }
|
||||
|
||||
context 'when no matches' do
|
||||
let(:paths) { ['unknown'] }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'when some matches' do
|
||||
let(:paths) { %w[unknown gitlabb] }
|
||||
|
||||
it { is_expected.to match_array([redirect_route]) }
|
||||
end
|
||||
|
||||
context 'when multiple matches' do
|
||||
let(:paths) { ['unknown', 'gitlabb', 'gitlabb/test'] }
|
||||
|
||||
it { is_expected.to match_array([redirect_route, redirect2]) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.matching_path_and_descendants' do
|
||||
let!(:redirect2) { group.redirect_routes.create!(path: 'gitlabb/test') }
|
||||
let!(:redirect3) { group.redirect_routes.create!(path: 'gitlabb/test/foo') }
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ require_relative '../../../../scripts/lib/glfm/update_example_snapshots'
|
|||
# However, only the `with full processing of static and WYSIWYG HTML` context is used
|
||||
# to test these slow sub-processes, and it only contains two examples.
|
||||
#
|
||||
# All other tests currently in the file pass the `skip_static_and_wysiwyg: true`
|
||||
# All other tests currently in the file pass the `skip_static: true`
|
||||
# flag to `#process`, which skips the slow sub-processes. All of these other tests
|
||||
# should run in sub-second time when the Spring pre-loader is used. This allows
|
||||
# logic which is not directly related to the slow sub-processes to be TDD'd with a
|
||||
|
|
@ -46,9 +46,6 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', :uses_fast_spec_helper_
|
|||
let(:es_html_yml_path) { described_class::ES_HTML_YML_PATH }
|
||||
let(:es_html_yml_io_existing) { StringIO.new(es_html_yml_io_existing_contents) }
|
||||
let(:es_html_yml_io) { StringIO.new }
|
||||
let(:es_prosemirror_json_yml_path) { described_class::ES_PROSEMIRROR_JSON_YML_PATH }
|
||||
let(:es_prosemirror_json_yml_io_existing) { StringIO.new(es_prosemirror_json_yml_io_existing_contents) }
|
||||
let(:es_prosemirror_json_yml_io) { StringIO.new }
|
||||
|
||||
# Internal tempfiles
|
||||
let(:static_html_tempfile_path) { Tempfile.new.path }
|
||||
|
|
@ -297,59 +294,16 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', :uses_fast_spec_helper_
|
|||
This entry is no longer exists in the snapshot_spec.md, so it will be deleted.
|
||||
static: |-
|
||||
This entry is no longer exists in the snapshot_spec.md, so it will be deleted.
|
||||
wysiwyg: |-
|
||||
This entry is no longer exists in the snapshot_spec.md, so it will be deleted.
|
||||
02_01_00__inlines__strong__001:
|
||||
canonical: |
|
||||
This entry is existing, but not skipped, so it will be overwritten.
|
||||
static: |-
|
||||
This entry is existing, but not skipped, so it will be overwritten.
|
||||
wysiwyg: |-
|
||||
This entry is existing, but not skipped, so it will be overwritten.
|
||||
05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001:
|
||||
canonical: |
|
||||
<p><strong>This example will have its manually modified static HTML, WYSIWYG HTML, and ProseMirror JSON preserved</strong></p>
|
||||
static: |-
|
||||
<p>This is the manually modified static HTML which will be preserved</p>
|
||||
wysiwyg: |-
|
||||
<p dir="auto">This is the manually modified WYSIWYG HTML which will be preserved</p>
|
||||
YAML
|
||||
end
|
||||
|
||||
let(:es_prosemirror_json_yml_io_existing_contents) do
|
||||
<<~YAML
|
||||
---
|
||||
01_00_00__obsolete_entry_to_be_deleted__001: |-
|
||||
{
|
||||
"obsolete": "This entry is no longer exists in the snapshot_spec.md, and is not skipped, so it will be deleted."
|
||||
}
|
||||
02_01_00__inlines__strong__001: |-
|
||||
{
|
||||
"existing": "This entry is existing, but not skipped, so it will be overwritten."
|
||||
}
|
||||
02_03_00__inlines__strikethrough_extension__001: |-
|
||||
{
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "~~Hi~~ Hello, world!"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
04_01_00__second_gitlab_specific_section_with_examples__strong_but_with_html__001: |-
|
||||
{
|
||||
"existing": "This entry is manually modified and preserved because skip_update_example_snapshot_prosemirror_json will be truthy"
|
||||
}
|
||||
05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001: |-
|
||||
{
|
||||
"existing": "This entry is manually modified and preserved because skip_update_example_snapshots will be truthy"
|
||||
}
|
||||
YAML
|
||||
end
|
||||
|
||||
|
|
@ -378,15 +332,12 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', :uses_fast_spec_helper_
|
|||
allow(File).to receive(:open).with(es_markdown_yml_path) { es_markdown_yml_io }
|
||||
allow(File).to receive(:open).with(es_html_yml_path, 'w') { es_html_yml_io }
|
||||
allow(File).to receive(:open).with(es_html_yml_path) { es_html_yml_io_existing }
|
||||
allow(File).to receive(:open).with(es_prosemirror_json_yml_path, 'w') { es_prosemirror_json_yml_io }
|
||||
allow(File).to receive(:open).with(es_prosemirror_json_yml_path) { es_prosemirror_json_yml_io_existing }
|
||||
|
||||
# Allow normal opening of Tempfile files created during script execution.
|
||||
tempfile_basenames = [
|
||||
described_class::MARKDOWN_TEMPFILE_BASENAME[0],
|
||||
described_class::METADATA_TEMPFILE_BASENAME[0],
|
||||
described_class::STATIC_HTML_TEMPFILE_BASENAME[0],
|
||||
described_class::WYSIWYG_HTML_AND_JSON_TEMPFILE_BASENAME[0]
|
||||
described_class::STATIC_HTML_TEMPFILE_BASENAME[0]
|
||||
].join('|')
|
||||
# NOTE: This approach with a single regex seems to be the only way this can work. If you
|
||||
# attempt to have multiple `allow...and_call_original` with `any_args`, the mocked
|
||||
|
|
@ -407,13 +358,13 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', :uses_fast_spec_helper_
|
|||
end
|
||||
|
||||
it 'still writes the example to examples_index.yml' do
|
||||
subject.process(skip_static_and_wysiwyg: true)
|
||||
subject.process(skip_static: true)
|
||||
|
||||
expect(es_examples_index_yml_contents).to match(expected_unskipped_example)
|
||||
end
|
||||
|
||||
it 'still writes the example to markdown.yml' do
|
||||
subject.process(skip_static_and_wysiwyg: true)
|
||||
subject.process(skip_static: true)
|
||||
|
||||
expect(es_markdown_yml_contents).to match(expected_unskipped_example)
|
||||
end
|
||||
|
|
@ -485,7 +436,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', :uses_fast_spec_helper_
|
|||
end
|
||||
|
||||
it 'writes the correct content' do
|
||||
subject.process(skip_static_and_wysiwyg: true)
|
||||
subject.process(skip_static: true)
|
||||
|
||||
expect(es_examples_index_yml_contents).to eq(expected_examples_index_yml_contents)
|
||||
end
|
||||
|
|
@ -528,62 +479,13 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', :uses_fast_spec_helper_
|
|||
end
|
||||
|
||||
it 'writes the correct content' do
|
||||
subject.process(skip_static_and_wysiwyg: true)
|
||||
subject.process(skip_static: true)
|
||||
|
||||
expect(es_markdown_yml_contents).to eq(expected_markdown_yml_contents)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'error handling when manually-curated input specification config files contain invalid example names:' do
|
||||
let(:err_msg) do
|
||||
/#{config_file}.*01_00_00__invalid__001.*does not have.*entry in.*#{described_class::ES_EXAMPLES_INDEX_YML_PATH}/m
|
||||
end
|
||||
|
||||
let(:invalid_example_name_file_contents) do
|
||||
<<~YAML
|
||||
---
|
||||
01_00_00__invalid__001:
|
||||
a: 1
|
||||
YAML
|
||||
end
|
||||
|
||||
context 'for glfm_example_status.yml' do
|
||||
let(:config_file) { described_class::GLFM_EXAMPLE_STATUS_YML_PATH }
|
||||
let(:glfm_example_status_yml_contents) { invalid_example_name_file_contents }
|
||||
|
||||
it 'raises error' do
|
||||
expect { subject.process(skip_static_and_wysiwyg: true) }.to raise_error(err_msg)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for glfm_example_metadata.yml' do
|
||||
let(:config_file) { described_class::GLFM_EXAMPLE_METADATA_YML_PATH }
|
||||
let(:glfm_example_metadata_yml_contents) { invalid_example_name_file_contents }
|
||||
|
||||
it 'raises error' do
|
||||
expect { subject.process(skip_static_and_wysiwyg: true) }.to raise_error(err_msg)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for glfm_example_normalizations.yml' do
|
||||
let(:config_file) { described_class::GLFM_EXAMPLE_NORMALIZATIONS_YML_PATH }
|
||||
let(:glfm_example_normalizations_yml_contents) { invalid_example_name_file_contents }
|
||||
|
||||
it 'raises error' do
|
||||
expect { subject.process(skip_static_and_wysiwyg: true) }.to raise_error(err_msg)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with full processing of static and WYSIWYG HTML' do
|
||||
before(:all) do # rubocop: disable RSpec/BeforeAll
|
||||
# NOTE: It is a necessary to do a `yarn install` in order to ensure that
|
||||
# `scripts/lib/glfm/render_wysiwyg_html_and_json.js` can be invoked successfully
|
||||
# on the CI job (which will not be set up for frontend specs since this is
|
||||
# an RSpec spec), or if the current yarn dependencies are not installed locally.
|
||||
described_class.new.run_external_cmd('yarn install --frozen-lockfile')
|
||||
end
|
||||
|
||||
context 'with full processing of static HTML' do
|
||||
describe 'manually-curated input specification config files' do
|
||||
let(:glfm_example_status_yml_contents) { '' }
|
||||
let(:glfm_example_metadata_yml_contents) { '' }
|
||||
|
|
@ -594,9 +496,8 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', :uses_fast_spec_helper_
|
|||
end
|
||||
end
|
||||
|
||||
describe 'writing html.yml and prosemirror_json.yml' do
|
||||
describe 'writing html.yml' do
|
||||
let(:es_html_yml_contents) { reread_io(es_html_yml_io) }
|
||||
let(:es_prosemirror_json_yml_contents) { reread_io(es_prosemirror_json_yml_io) }
|
||||
|
||||
# NOTE: This example_status.yml is crafted in conjunction with expected_html_yml_contents
|
||||
# to test the behavior of the `skip_update_*` flags
|
||||
|
|
@ -630,34 +531,24 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', :uses_fast_spec_helper_
|
|||
<p><strong>bold</strong></p>
|
||||
static: |-
|
||||
<p data-sourcepos="1:1-1:8" dir="auto"><strong data-sourcepos="1:1-1:8">bold</strong></p>
|
||||
wysiwyg: |-
|
||||
<p dir="auto"><strong>bold</strong></p>
|
||||
02_01_00__inlines__strong__002:
|
||||
canonical: |
|
||||
<p><strong>bold with more text</strong></p>
|
||||
static: |-
|
||||
<p data-sourcepos="1:1-1:23" dir="auto"><strong data-sourcepos="1:1-1:23">bold with more text</strong></p>
|
||||
wysiwyg: |-
|
||||
<p dir="auto"><strong>bold with more text</strong></p>
|
||||
02_03_00__inlines__strikethrough_extension__001:
|
||||
canonical: |
|
||||
<p><del>Hi</del> Hello, world!</p>
|
||||
static: |-
|
||||
<p data-sourcepos="1:1-1:20" dir="auto"><del data-sourcepos="1:1-1:6">Hi</del> Hello, world!</p>
|
||||
wysiwyg: |-
|
||||
<p dir="auto"><s>Hi</s> Hello, world!</p>
|
||||
03_01_00__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001:
|
||||
canonical: |
|
||||
<p><strong>bold</strong></p>
|
||||
wysiwyg: |-
|
||||
<p dir="auto"><strong>bold</strong></p>
|
||||
03_02_01__first_gitlab_specific_section_with_examples__h2_which_contains_an_h3__example_in_an_h3__001:
|
||||
canonical: |
|
||||
<p>Example in an H3</p>
|
||||
static: |-
|
||||
<p data-sourcepos="1:1-1:16" dir="auto">Example in an H3</p>
|
||||
wysiwyg: |-
|
||||
<p dir="auto">Example in an H3</p>
|
||||
04_01_00__second_gitlab_specific_section_with_examples__strong_but_with_html__001:
|
||||
canonical: |
|
||||
<p><strong>
|
||||
|
|
@ -672,263 +563,34 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', :uses_fast_spec_helper_
|
|||
<p><strong>This example will have its manually modified static HTML, WYSIWYG HTML, and ProseMirror JSON preserved</strong></p>
|
||||
static: |-
|
||||
<p>This is the manually modified static HTML which will be preserved</p>
|
||||
wysiwyg: |-
|
||||
<p dir="auto">This is the manually modified WYSIWYG HTML which will be preserved</p>
|
||||
06_01_00__api_request_overrides__group_upload_link__001:
|
||||
canonical: |
|
||||
<p><a href="groups-test-file">groups-test-file</a></p>
|
||||
static: |-
|
||||
<p data-sourcepos="1:1-1:45" dir="auto"><a data-sourcepos="1:1-1:45" href="/-/group/66666/uploads/groups-test-file" data-canonical-src="/uploads/groups-test-file" data-link="true" class="gfm">groups-test-file</a></p>
|
||||
wysiwyg: |-
|
||||
<p dir="auto"><a target="_blank" rel="noopener noreferrer nofollow" href="/uploads/groups-test-file">groups-test-file</a></p>
|
||||
06_02_00__api_request_overrides__project_repo_link__001:
|
||||
canonical: |
|
||||
<p><a href="projects-test-file">projects-test-file</a></p>
|
||||
static: |-
|
||||
<p data-sourcepos="1:1-1:40" dir="auto"><a data-sourcepos="1:1-1:40" href="/glfm_group/glfm_project/-/blob/master/projects-test-file" class="gfm">projects-test-file</a></p>
|
||||
wysiwyg: |-
|
||||
<p dir="auto"><a target="_blank" rel="noopener noreferrer nofollow" href="projects-test-file">projects-test-file</a></p>
|
||||
06_03_00__api_request_overrides__project_snippet_ref__001:
|
||||
canonical: |
|
||||
<p>This project snippet ID reference IS filtered: <a href="/glfm_group/glfm_project/-/snippets/88888">$88888</a>
|
||||
static: |-
|
||||
<p data-sourcepos="1:1-1:53" dir="auto">This project snippet ID reference IS filtered: <a href="/glfm_group/glfm_project/-/snippets/88888" data-reference-type="snippet" data-original="$88888" data-link="false" data-link-reference="false" data-snippet="88888" data-project="77777" data-container="body" data-placement="top" title="glfm_project_snippet" class="gfm gfm-snippet has-tooltip">$88888</a></p>
|
||||
wysiwyg: |-
|
||||
<p dir="auto">This project snippet ID reference IS filtered: $88888</p>
|
||||
06_04_00__api_request_overrides__personal_snippet_ref__001:
|
||||
canonical: |
|
||||
<p>This personal snippet ID reference is NOT filtered: $99999</p>
|
||||
static: |-
|
||||
<p data-sourcepos="1:1-1:58" dir="auto">This personal snippet ID reference is NOT filtered: $99999</p>
|
||||
wysiwyg: |-
|
||||
<p dir="auto">This personal snippet ID reference is NOT filtered: $99999</p>
|
||||
06_05_00__api_request_overrides__project_wiki_link__001:
|
||||
canonical: |
|
||||
<p><a href="project-wikis-test-file">project-wikis-test-file</a></p>
|
||||
static: |-
|
||||
<p data-sourcepos="1:1-1:50" dir="auto"><a data-sourcepos="1:1-1:50" href="/glfm_group/glfm_project/-/wikis/project-wikis-test-file" data-canonical-src="project-wikis-test-file">project-wikis-test-file</a></p>
|
||||
wysiwyg: |-
|
||||
<p dir="auto"><a target="_blank" rel="noopener noreferrer nofollow" href="project-wikis-test-file">project-wikis-test-file</a></p>
|
||||
YAML
|
||||
end
|
||||
|
||||
let(:expected_prosemirror_json_contents) do
|
||||
<<~YAML
|
||||
---
|
||||
02_01_00__inlines__strong__001: |-
|
||||
{
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"marks": [
|
||||
{
|
||||
"type": "bold"
|
||||
}
|
||||
],
|
||||
"text": "bold"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
02_03_00__inlines__strikethrough_extension__001: |-
|
||||
{
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"marks": [
|
||||
{
|
||||
"type": "strike",
|
||||
"attrs": {
|
||||
"htmlTag": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"text": "Hi"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": " Hello, world!"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
03_01_00__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001: |-
|
||||
{
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"marks": [
|
||||
{
|
||||
"type": "bold"
|
||||
}
|
||||
],
|
||||
"text": "bold"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
03_02_01__first_gitlab_specific_section_with_examples__h2_which_contains_an_h3__example_in_an_h3__001: |-
|
||||
{
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Example in an H3"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
04_01_00__second_gitlab_specific_section_with_examples__strong_but_with_html__001: |-
|
||||
{
|
||||
"existing": "This entry is manually modified and preserved because skip_update_example_snapshot_prosemirror_json will be truthy"
|
||||
}
|
||||
05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001: |-
|
||||
{
|
||||
"existing": "This entry is manually modified and preserved because skip_update_example_snapshots will be truthy"
|
||||
}
|
||||
06_01_00__api_request_overrides__group_upload_link__001: |-
|
||||
{
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"marks": [
|
||||
{
|
||||
"type": "link",
|
||||
"attrs": {
|
||||
"uploading": false,
|
||||
"href": "/uploads/groups-test-file",
|
||||
"title": null,
|
||||
"isGollumLink": false,
|
||||
"isWikiPage": false,
|
||||
"canonicalSrc": "/uploads/groups-test-file",
|
||||
"isReference": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"text": "groups-test-file"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
06_02_00__api_request_overrides__project_repo_link__001: |-
|
||||
{
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"marks": [
|
||||
{
|
||||
"type": "link",
|
||||
"attrs": {
|
||||
"uploading": false,
|
||||
"href": "projects-test-file",
|
||||
"title": null,
|
||||
"isGollumLink": false,
|
||||
"isWikiPage": false,
|
||||
"canonicalSrc": "projects-test-file",
|
||||
"isReference": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"text": "projects-test-file"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
06_03_00__api_request_overrides__project_snippet_ref__001: |-
|
||||
{
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "This project snippet ID reference IS filtered: $88888"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
06_04_00__api_request_overrides__personal_snippet_ref__001: |-
|
||||
{
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "This personal snippet ID reference is NOT filtered: $99999"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
06_05_00__api_request_overrides__project_wiki_link__001: |-
|
||||
{
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"marks": [
|
||||
{
|
||||
"type": "link",
|
||||
"attrs": {
|
||||
"uploading": false,
|
||||
"href": "project-wikis-test-file",
|
||||
"title": null,
|
||||
"isGollumLink": false,
|
||||
"isWikiPage": false,
|
||||
"canonicalSrc": "project-wikis-test-file",
|
||||
"isReference": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"text": "project-wikis-test-file"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
YAML
|
||||
end
|
||||
|
||||
# NOTE: Both `html.yml` and `prosemirror_json.yml` generation are tested in a single example, to
|
||||
# avoid slower tests, because generating the static HTML is slow due to the need to invoke
|
||||
# the rails environment. We could have separate sections, but this would require an extra flag
|
||||
# to the `process` method to independently skip static vs. WYSIWYG, which is not worth the effort.
|
||||
it 'writes the correct content', :unlimited_max_formatted_output_length do
|
||||
# expectation that skipping message is only output once per example
|
||||
expect(subject).to receive(:output).once.with(/reason.*skipping this example because it is very bad/i)
|
||||
|
|
@ -936,7 +598,6 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', :uses_fast_spec_helper_
|
|||
subject.process
|
||||
|
||||
expect(es_html_yml_contents).to eq(expected_html_yml_contents)
|
||||
expect(es_prosemirror_json_yml_contents).to eq(expected_prosemirror_json_contents)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,5 +17,12 @@ module Features
|
|||
def within_testid(testid, context: page, **kwargs, &block)
|
||||
context.within("[data-testid='#{testid}']", **kwargs, &block)
|
||||
end
|
||||
|
||||
def page_breadcrumbs
|
||||
all('[data-testid=breadcrumb-links] a').map do |a|
|
||||
# We use `.dom_attribute` because Selenium transforms `.attribute('href')` to include the full URL.
|
||||
{ text: a.text, href: a.native.dom_attribute('href') }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -167,6 +167,13 @@ module GraphqlHelpers
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
# create a valid query context object
|
||||
def query_context(user: current_user)
|
||||
query = GraphQL::Query.new(empty_schema, document: nil, context: {}, variables: {})
|
||||
GraphQL::Query::Context.new(query: query, values: { current_user: user })
|
||||
end
|
||||
|
||||
# rubocop:enable Metrics/ParameterLists
|
||||
|
||||
# Pros:
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ RSpec.shared_examples 'work items rolled up dates' do
|
|||
context 'when removing all children' do
|
||||
it 'rolled up child dates' do
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/473408
|
||||
allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(107)
|
||||
allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(108)
|
||||
|
||||
add_new_child(title: 'child issue 1', start_date: '2020-11-01', due_date: '2020-12-02')
|
||||
add_new_child(title: 'child issue 2', start_date: '2020-12-01', due_date: '2021-01-02')
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'update board list mutation' do
|
||||
include GraphqlHelpers
|
||||
|
||||
describe '#resolve' do
|
||||
let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
|
||||
let(:mutation) { described_class.new(object: nil, context: query_context, field: nil) }
|
||||
let(:list_update_params) { { position: 1, collapsed: true } }
|
||||
|
||||
subject { mutation.resolve(list: list, **list_update_params) }
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ require 'spec_helper'
|
|||
RSpec.shared_examples 'board lists create mutation' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
|
||||
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
|
||||
let(:mutation) { described_class.new(object: nil, context: query_context, field: nil) }
|
||||
let(:list_create_params) { {} }
|
||||
|
||||
subject { mutation.resolve(board_id: board.to_global_id, **list_create_params) }
|
||||
|
|
@ -27,7 +27,7 @@ RSpec.shared_examples 'board lists create mutation' do
|
|||
describe '#resolve' do
|
||||
context 'with proper permissions' do
|
||||
before_all do
|
||||
group.add_reporter(user)
|
||||
group.add_reporter(current_user)
|
||||
end
|
||||
|
||||
describe 'backlog list' do
|
||||
|
|
@ -76,7 +76,7 @@ RSpec.shared_examples 'board lists create mutation' do
|
|||
|
||||
context 'without proper permissions' do
|
||||
before_all do
|
||||
group.add_guest(user)
|
||||
group.add_guest(current_user)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue