Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-02-07 15:15:53 +00:00
parent 072dbf4b87
commit 468bcfb9c6
84 changed files with 489 additions and 1369 deletions

View File

@ -1 +1 @@
08c42adfc68c06661cf52363dacf52e2c347adff
87a19146fdc01d0132a278b661017705514207cf

View File

@ -310,7 +310,7 @@ gem 'pg_query', '~> 2.1'
gem 'premailer-rails', '~> 1.10.3'
# LabKit: Tracing and Correlation
gem 'gitlab-labkit', '~> 0.21.3'
gem 'gitlab-labkit', '~> 0.22.0'
# Thrift is a dependency of gitlab-labkit, we want a version higher than 0.14.0
# because of https://gitlab.com/gitlab-org/gitlab/-/issues/321900
gem 'thrift', '>= 0.14.0'

View File

@ -470,11 +470,11 @@ GEM
fog-json (~> 1.2.0)
mime-types
ms_rest_azure (~> 0.12.0)
gitlab-labkit (0.21.3)
gitlab-labkit (0.22.0)
actionpack (>= 5.0.0, < 7.0.0)
activesupport (>= 5.0.0, < 7.0.0)
grpc (>= 1.37)
jaeger-client (~> 1.1)
jaeger-client (~> 1.1.0)
opentracing (~> 0.4)
pg_query (~> 2.1)
redis (> 3.0.0, < 5.0.0)
@ -1470,7 +1470,7 @@ DEPENDENCIES
gitlab-dangerfiles (~> 2.8.0)
gitlab-experiment (~> 0.7.0)
gitlab-fog-azure-rm (~> 1.2.0)
gitlab-labkit (~> 0.21.3)
gitlab-labkit (~> 0.22.0)
gitlab-license (~> 2.1.0)
gitlab-license_finder (~> 6.0)
gitlab-mail_room (~> 0.0.9)

View File

@ -1,4 +1,4 @@
import Store from 'ee_else_ce/sidebar/stores/sidebar_store';
import Store from '~/sidebar/stores/sidebar_store';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';

View File

@ -1,48 +0,0 @@
<script>
import { GlDatepicker } from '@gitlab/ui';
import { pikadayToString } from '~/lib/utils/datetime_utility';
export default {
name: 'DatePicker',
components: {
GlDatepicker,
},
props: {
selectedDate: {
type: Date,
required: false,
default: null,
},
minDate: {
type: Date,
required: false,
default: null,
},
maxDate: {
type: Date,
required: false,
default: null,
},
},
methods: {
selected(date) {
this.$emit('newDateSelected', pikadayToString(date));
},
toggled() {
this.$emit('hidePicker');
},
},
};
</script>
<template>
<gl-datepicker
:value="selectedDate"
:min-date="minDate"
:max-date="maxDate"
start-opened
@close="toggled"
@click="toggled"
@input="selected"
/>
</template>

View File

@ -1,49 +0,0 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
export default {
name: 'CollapsedCalendarIcon',
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
GlIcon,
},
props: {
containerClass: {
type: String,
required: false,
default: '',
},
text: {
type: String,
required: false,
default: '',
},
showIcon: {
type: Boolean,
required: false,
default: true,
},
tooltipText: {
type: String,
required: false,
default: '',
},
},
methods: {
click() {
this.$emit('click');
},
},
};
</script>
<template>
<div v-gl-tooltip.left.viewport="tooltipText" :class="containerClass" @click="click">
<gl-icon v-if="showIcon" name="calendar" />
<slot>
<span> {{ text }} </span>
</slot>
</div>
</template>

View File

@ -1,148 +0,0 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { dateInWords } from '../../../lib/utils/datetime_utility';
import datePicker from '../pikaday.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
import toggleSidebar from './toggle_sidebar.vue';
export default {
name: 'SidebarDatePicker',
components: {
datePicker,
toggleSidebar,
collapsedCalendarIcon,
GlLoadingIcon,
},
props: {
blockClass: {
type: String,
required: false,
default: '',
},
collapsed: {
type: Boolean,
required: false,
default: true,
},
showToggleSidebar: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
editable: {
type: Boolean,
required: false,
default: false,
},
label: {
type: String,
required: false,
default: __('Date picker'),
},
selectedDate: {
type: Date,
required: false,
default: null,
},
minDate: {
type: Date,
required: false,
default: null,
},
maxDate: {
type: Date,
required: false,
default: null,
},
},
data() {
return {
editing: false,
};
},
computed: {
selectedAndEditable() {
return this.selectedDate && this.editable;
},
selectedDateWords() {
return dateInWords(this.selectedDate, true);
},
collapsedText() {
return this.selectedDateWords ? this.selectedDateWords : __('None');
},
},
methods: {
stopEditing() {
this.editing = false;
},
toggleDatePicker() {
this.editing = !this.editing;
},
newDateSelected(date = null) {
this.date = date;
this.editing = false;
this.$emit('saveDate', date);
},
toggleSidebar() {
this.$emit('toggleCollapse');
},
},
};
</script>
<template>
<div :class="blockClass" class="block">
<div class="issuable-sidebar-header">
<toggle-sidebar :collapsed="collapsed" @toggle="toggleSidebar" />
</div>
<collapsed-calendar-icon :text="collapsedText" class="sidebar-collapsed-icon" />
<div class="title">
{{ label }}
<gl-loading-icon v-if="isLoading" size="sm" :inline="true" />
<div class="float-right">
<button
v-if="editable && !editing"
type="button"
class="btn-blank btn-link btn-primary-hover-link btn-sidebar-action"
@click="toggleDatePicker"
>
{{ __('Edit') }}
</button>
<toggle-sidebar v-if="showToggleSidebar" :collapsed="collapsed" @toggle="toggleSidebar" />
</div>
</div>
<div class="value">
<date-picker
v-if="editing"
:selected-date="selectedDate"
:min-date="minDate"
:max-date="maxDate"
:label="label"
@newDateSelected="newDateSelected"
@hidePicker="stopEditing"
/>
<span v-else class="value-content">
<template v-if="selectedDate">
<strong>{{ selectedDateWords }}</strong>
<span v-if="selectedAndEditable" class="no-value">
-
<button
type="button"
class="btn-blank btn-link btn-secondary-hover-link"
@click="newDateSelected(null)"
>
{{ __('remove') }}
</button>
</span>
</template>
<span v-else class="no-value">{{ __('None') }}</span>
</span>
</div>
</div>
</template>

View File

@ -1,37 +0,0 @@
<script>
import { GlDropdown, GlDropdownForm, GlDropdownDivider } from '@gitlab/ui';
export default {
components: {
GlDropdownForm,
GlDropdown,
GlDropdownDivider,
},
props: {
headerText: {
type: String,
required: true,
},
text: {
type: String,
required: true,
},
},
};
</script>
<template>
<gl-dropdown class="show" :text="text" @toggle="$emit('toggle')">
<template #header>
<p class="gl-font-weight-bold gl-text-center gl-mt-2 gl-mb-4">{{ headerText }}</p>
<gl-dropdown-divider />
<slot name="search"></slot>
</template>
<gl-dropdown-form>
<slot name="items"></slot>
</gl-dropdown-form>
<template #footer>
<slot name="footer"></slot>
</template>
</gl-dropdown>
</template>

View File

@ -1,34 +0,0 @@
<script>
export default {
props: {
colors: {
type: Array,
required: true,
validator(value) {
return value.length === 2;
},
},
opacity: {
type: Array,
required: true,
validator(value) {
return value.length === 2;
},
},
identifierName: {
type: String,
required: true,
},
},
};
</script>
<template>
<svg height="0" width="0">
<defs>
<linearGradient :id="identifierName">
<stop :stop-color="colors[0]" :stop-opacity="opacity[0]" offset="0%" />
<stop :stop-color="colors[1]" :stop-opacity="opacity[1]" offset="100%" />
</linearGradient>
</defs>
</svg>
</template>

View File

@ -366,29 +366,6 @@
background-color: transparent;
border-color: transparent;
}
&.btn-secondary-hover-link,
&.btn-default-hover-link {
color: $gl-text-color-secondary;
&:hover,
&:active,
&:focus {
color: $blue-600;
text-decoration: none;
}
}
&.btn-primary-hover-link {
color: inherit;
&:hover,
&:active,
&:focus {
color: $blue-600;
text-decoration: none;
}
}
}
// The .btn-svg class is available for legacy icon buttons to

View File

@ -415,49 +415,6 @@ $top-nav-hover-bg: var(--indigo-900-alpha-008, $indigo-900-alpha-008) !important
}
}
.title-container,
.navbar-nav {
.badge.badge-pill:not(.gl-badge) {
position: inherit;
font-weight: $gl-font-weight-normal;
margin-left: -6px;
font-size: 11px;
color: var(--gray-950, $white);
padding: 0 5px;
line-height: 12px;
border-radius: 7px;
box-shadow: 0 1px 0 rgba($gl-header-color, 0.2);
&.green-badge {
background-color: var(--green-400, $green-400);
}
&.merge-requests-count {
background-color: var(--orange-400, $orange-400);
}
&.todos-count {
background-color: var(--blue-400, $blue-400);
}
}
.canary-badge {
.badge {
font-size: $gl-font-size-small;
line-height: $gl-line-height;
padding: 0 $grid-size;
}
&:hover {
text-decoration: none;
.badge {
text-decoration: none;
}
}
}
}
@include media-breakpoint-down(xs) {
.navbar-gitlab .container-fluid {
font-size: 18px;

View File

@ -42,12 +42,6 @@ $status-box-line-height: 26px;
}
.milestone-content {
.issues-count {
margin-right: 17px;
float: right;
width: 105px;
}
.issuable-row {
span {
a {

View File

@ -274,16 +274,10 @@
font-weight: $gl-font-weight-normal;
}
.no-value,
.btn-default-hover-link,
.btn-secondary-hover-link {
.no-value {
color: $gl-text-color-secondary;
}
.btn-secondary-hover-link:hover {
color: $blue-600;
}
.sidebar-collapsed-icon {
display: none;
}

View File

@ -7,12 +7,8 @@ body.gl-dark {
--gray-100: #404040;
--gray-600: #bfbfbf;
--gray-900: #fafafa;
--gray-950: #fff;
--green-100: #0d532a;
--green-400: #108548;
--green-700: #91d4a8;
--blue-400: #1f75cb;
--orange-400: #ab6100;
--indigo-900-alpha-008: rgba(235, 235, 250, 0.08);
--gl-text-color: #fafafa;
--border-color: #4f4f4f;
@ -314,10 +310,18 @@ h1 {
padding-left: 0.6em;
border-radius: 10rem;
}
.badge-success {
color: #fff;
background-color: #2da160;
}
.badge-info {
color: #fff;
background-color: #428fdc;
}
.badge-warning {
color: #fff;
background-color: #c17d10;
}
.bg-transparent {
background-color: transparent !important;
}
@ -394,6 +398,34 @@ a.gl-badge.badge-info:active {
0 0 0 1px rgba(51, 51, 51, 0.4), 0 0 0 4px rgba(66, 143, 220, 0.48);
outline: none;
}
.gl-badge.badge-success {
background-color: #0d532a;
color: #91d4a8;
}
a.gl-badge.badge-success.active,
a.gl-badge.badge-success:active {
color: #ecf4ee;
background-color: #24663b;
}
a.gl-badge.badge-success:active {
box-shadow: inset 0 0 0 1px rgba(51, 51, 51, 0.8),
0 0 0 1px rgba(51, 51, 51, 0.4), 0 0 0 4px rgba(66, 143, 220, 0.48);
outline: none;
}
.gl-badge.badge-warning {
background-color: #703800;
color: #e9be74;
}
a.gl-badge.badge-warning.active,
a.gl-badge.badge-warning:active {
color: #fdf1dd;
background-color: #8f4700;
}
a.gl-badge.badge-warning:active {
box-shadow: inset 0 0 0 1px rgba(51, 51, 51, 0.8),
0 0 0 1px rgba(51, 51, 51, 0.4), 0 0 0 4px rgba(66, 143, 220, 0.48);
outline: none;
}
.gl-button .gl-badge {
top: 0;
}
@ -920,36 +952,6 @@ input {
line-height: 18px;
margin: 4px 0 4px 2px;
}
.title-container .badge.badge-pill:not(.gl-badge),
.navbar-nav .badge.badge-pill:not(.gl-badge) {
position: inherit;
font-weight: 400;
margin-left: -6px;
font-size: 11px;
color: var(--gray-950, #333);
padding: 0 5px;
line-height: 12px;
border-radius: 7px;
box-shadow: 0 1px 0 rgba(76, 78, 84, 0.2);
}
.title-container .badge.badge-pill:not(.gl-badge).green-badge,
.navbar-nav .badge.badge-pill:not(.gl-badge).green-badge {
background-color: var(--green-400, #108548);
}
.title-container .badge.badge-pill:not(.gl-badge).merge-requests-count,
.navbar-nav .badge.badge-pill:not(.gl-badge).merge-requests-count {
background-color: var(--orange-400, #ab6100);
}
.title-container .badge.badge-pill:not(.gl-badge).todos-count,
.navbar-nav .badge.badge-pill:not(.gl-badge).todos-count {
background-color: var(--blue-400, #1f75cb);
}
.title-container .canary-badge .badge,
.navbar-nav .canary-badge .badge {
font-size: 12px;
line-height: 16px;
padding: 0 0.5rem;
}
@media (max-width: 575.98px) {
.navbar-gitlab .container-fluid {
font-size: 18px;
@ -2022,18 +2024,9 @@ body.gl-dark {
white-space: nowrap;
width: 1px;
}
.gl-bg-green-500 {
background-color: #2da160;
}
.gl-border-none\! {
border-style: none !important;
}
.gl-rounded-pill {
border-radius: 0.75rem;
}
.gl-text-white {
color: #333;
}
.gl-display-none {
display: none;
}
@ -2055,9 +2048,8 @@ body.gl-dark {
.gl-pr-2 {
padding-right: 0.25rem;
}
.gl-py-1 {
padding-top: 0.125rem;
padding-bottom: 0.125rem;
.gl-ml-n2 {
margin-left: -0.25rem;
}
.gl-ml-3 {
margin-left: 0.5rem;

View File

@ -295,10 +295,18 @@ h1 {
padding-left: 0.6em;
border-radius: 10rem;
}
.badge-success {
color: #fff;
background-color: #108548;
}
.badge-info {
color: #fff;
background-color: #1f75cb;
}
.badge-warning {
color: #fff;
background-color: #ab6100;
}
.bg-transparent {
background-color: transparent !important;
}
@ -375,6 +383,34 @@ a.gl-badge.badge-info:active {
0 0 0 1px rgba(255, 255, 255, 0.4), 0 0 0 4px rgba(31, 117, 203, 0.48);
outline: none;
}
.gl-badge.badge-success {
background-color: #c3e6cd;
color: #24663b;
}
a.gl-badge.badge-success.active,
a.gl-badge.badge-success:active {
color: #0a4020;
background-color: #91d4a8;
}
a.gl-badge.badge-success:active {
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.8),
0 0 0 1px rgba(255, 255, 255, 0.4), 0 0 0 4px rgba(31, 117, 203, 0.48);
outline: none;
}
.gl-badge.badge-warning {
background-color: #f5d9a8;
color: #8f4700;
}
a.gl-badge.badge-warning.active,
a.gl-badge.badge-warning:active {
color: #5c2900;
background-color: #e9be74;
}
a.gl-badge.badge-warning:active {
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.8),
0 0 0 1px rgba(255, 255, 255, 0.4), 0 0 0 4px rgba(31, 117, 203, 0.48);
outline: none;
}
.gl-button .gl-badge {
top: 0;
}
@ -901,36 +937,6 @@ input {
line-height: 18px;
margin: 4px 0 4px 2px;
}
.title-container .badge.badge-pill:not(.gl-badge),
.navbar-nav .badge.badge-pill:not(.gl-badge) {
position: inherit;
font-weight: 400;
margin-left: -6px;
font-size: 11px;
color: var(--gray-950, #fff);
padding: 0 5px;
line-height: 12px;
border-radius: 7px;
box-shadow: 0 1px 0 rgba(76, 78, 84, 0.2);
}
.title-container .badge.badge-pill:not(.gl-badge).green-badge,
.navbar-nav .badge.badge-pill:not(.gl-badge).green-badge {
background-color: var(--green-400, #2da160);
}
.title-container .badge.badge-pill:not(.gl-badge).merge-requests-count,
.navbar-nav .badge.badge-pill:not(.gl-badge).merge-requests-count {
background-color: var(--orange-400, #c17d10);
}
.title-container .badge.badge-pill:not(.gl-badge).todos-count,
.navbar-nav .badge.badge-pill:not(.gl-badge).todos-count {
background-color: var(--blue-400, #428fdc);
}
.title-container .canary-badge .badge,
.navbar-nav .canary-badge .badge {
font-size: 12px;
line-height: 16px;
padding: 0 0.5rem;
}
@media (max-width: 575.98px) {
.navbar-gitlab .container-fluid {
font-size: 18px;
@ -1691,18 +1697,9 @@ svg.s16 {
white-space: nowrap;
width: 1px;
}
.gl-bg-green-500 {
background-color: #108548;
}
.gl-border-none\! {
border-style: none !important;
}
.gl-rounded-pill {
border-radius: 0.75rem;
}
.gl-text-white {
color: #fff;
}
.gl-display-none {
display: none;
}
@ -1724,9 +1721,8 @@ svg.s16 {
.gl-pr-2 {
padding-right: 0.25rem;
}
.gl-py-1 {
padding-top: 0.125rem;
padding-bottom: 0.125rem;
.gl-ml-n2 {
margin-left: -0.25rem;
}
.gl-ml-3 {
margin-left: 0.5rem;

View File

@ -16,7 +16,7 @@ class Admin::UsersController < Admin::ApplicationController
return redirect_to admin_cohorts_path if params[:tab] == 'cohorts'
@users = User.filter_items(params[:filter]).order_name_asc
@users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present?
@users = @users.search(params[:search_query], with_private_emails: true) if params[:search_query].present?
@users = users_with_included_associations(@users)
@users = @users.sort_by_attribute(@sort = params[:sort])
@users = @users.page(params[:page])

View File

@ -62,7 +62,7 @@ module Autocomplete
find_users
.active
.reorder_by_name
.optionally_search(search)
.optionally_search(search, use_minimum_char_limit: use_minimum_char_limit)
.where_not_in(skip_users)
.limit_to_todo_authors(
user: current_user,
@ -99,6 +99,12 @@ module Autocomplete
ActiveRecord::Associations::Preloader.new.preload(items, :status)
end
# rubocop: enable CodeReuse/ActiveRecord
def use_minimum_char_limit
return if project.blank? && group.blank? # We return nil so that we use the default defined in the User model
false
end
end
end

View File

@ -8,7 +8,7 @@ module Mutations
ADMIN_MESSAGE = 'You must be an admin to use this mutation'
::Gitlab::ApplicationContext::KNOWN_KEYS.each do |key|
::Gitlab::ApplicationContext.known_keys.each do |key|
argument key,
GraphQL::Types::String,
required: false,

View File

@ -204,7 +204,7 @@ class Member < ApplicationRecord
class << self
def search(query)
joins(:user).merge(User.search(query))
joins(:user).merge(User.search(query, use_minimum_char_limit: false))
end
def search_invite_email(query)

View File

@ -668,7 +668,8 @@ class User < ApplicationRecord
sanitized_order_sql = Arel.sql(sanitize_sql_array([order, query: query]))
scope = options[:with_private_emails] ? search_with_secondary_emails(query) : search_with_public_emails(query)
scope = options[:with_private_emails] ? with_primary_or_secondary_email(query) : with_public_email(query)
scope = scope.or(search_by_name_or_username(query, use_minimum_char_limit: options[:use_minimum_char_limit]))
scope.reorder(sanitized_order_sql, :name)
end
@ -685,50 +686,32 @@ class User < ApplicationRecord
reorder(:name)
end
def search_with_public_emails(query)
return none if query.blank?
query = query.downcase
where(
fuzzy_arel_match(:name, query, use_minimum_char_limit: user_search_minimum_char_limit)
.or(fuzzy_arel_match(:username, query, use_minimum_char_limit: user_search_minimum_char_limit))
.or(arel_table[:public_email].eq(query))
)
end
def search_without_secondary_emails(query)
return none if query.blank?
query = query.downcase
where(
fuzzy_arel_match(:name, query, lower_exact_match: true)
.or(fuzzy_arel_match(:username, query, lower_exact_match: true))
.or(arel_table[:email].eq(query))
)
end
# searches user by given pattern
# it compares name, email, username fields and user's secondary emails with given pattern
# it compares name and username fields with given pattern
# This method uses ILIKE on PostgreSQL.
def search_by_name_or_username(query, use_minimum_char_limit: nil)
use_minimum_char_limit = user_search_minimum_char_limit if use_minimum_char_limit.nil?
def search_with_secondary_emails(query)
return none if query.blank?
where(
fuzzy_arel_match(:name, query, use_minimum_char_limit: use_minimum_char_limit)
.or(fuzzy_arel_match(:username, query, use_minimum_char_limit: use_minimum_char_limit))
)
end
query = query.downcase
def with_public_email(email_address)
where(public_email: email_address)
end
def with_primary_or_secondary_email(email_address)
email_table = Email.arel_table
matched_by_email_user_id = email_table
.project(email_table[:user_id])
.where(email_table[:email].eq(query))
.where(email_table[:email].eq(email_address))
.take(1) # at most 1 record as there is a unique constraint
where(
fuzzy_arel_match(:name, query, use_minimum_char_limit: user_search_minimum_char_limit)
.or(fuzzy_arel_match(:username, query, use_minimum_char_limit: user_search_minimum_char_limit))
.or(arel_table[:email].eq(query))
.or(arel_table[:id].eq(matched_by_email_user_id))
arel_table[:email].eq(email_address)
.or(arel_table[:id].eq(matched_by_email_user_id))
)
end

View File

@ -32,7 +32,7 @@ class UsersStarProject < ApplicationRecord
end
def search(query)
joins(:user).merge(User.search(query))
joins(:user).merge(User.search(query, use_minimum_char_limit: false))
end
end
end

View File

@ -70,7 +70,9 @@ module Ci
def check_assignable_runners!(build); end
def clone_build(build)
project.builds.new(build_attributes(build))
project.builds.new(build_attributes(build)).tap do |new_build|
yield(new_build) if block_given?
end
end
def build_attributes(build)

View File

@ -2,7 +2,7 @@
module MergeRequests
class RebaseService < MergeRequests::BaseService
REBASE_ERROR = 'Rebase failed. Please rebase locally'
REBASE_ERROR = 'Rebase failed: Rebase locally, resolve all conflicts, then push the branch.'
attr_reader :merge_request, :rebase_error
@ -35,7 +35,7 @@ module MergeRequests
def set_rebase_error(exception)
@rebase_error =
if exception.is_a?(Gitlab::Git::PreReceiveError)
"Something went wrong during the rebase pre-receive hook: #{exception.message}."
"The rebase pre-receive hook failed: #{exception.message}."
else
REBASE_ERROR
end

View File

@ -9,9 +9,9 @@ module MergeRequests
return success(squash_sha: merge_request.diff_head_sha)
end
return error(s_('MergeRequests|This project does not allow squashing commits when merge requests are accepted.')) if squash_forbidden?
return error(s_("MergeRequests|Squashing not allowed: This project doesn't allow you to squash commits when merging.")) if squash_forbidden?
squash! || error(s_('MergeRequests|Failed to squash. Should be done manually.'))
squash! || error(s_('MergeRequests|Squashing failed: Squash the commits locally, resolve any conflicts, then push the branch.'))
end
private

View File

@ -16,7 +16,7 @@
= logo_text
- if Gitlab.com_and_canary?
= link_to Gitlab::Saas.canary_toggle_com_url, class: 'canary-badge bg-transparent', data: { qa_selector: 'canary_badge_link' }, target: :_blank, rel: 'noopener noreferrer' do
%span.gl-badge.gl-bg-green-500.gl-text-white.gl-rounded-pill.gl-font-weight-bold.gl-py-1
= gl_badge_tag({ variant: :success }) do
= _('Next')
- if current_user
@ -64,7 +64,7 @@
container: 'body' } do
= sprite_icon('issues')
- issues_count = assigned_issuables_count(:issues)
%span.badge.badge-pill.issues-count.green-badge{ class: ('hidden' if issues_count == 0) }
= gl_badge_tag({ size: :sm, variant: :success }, { class: "gl-ml-n2 #{(' gl-display-none' if issues_count == 0)}", "aria-label": n_("%d assigned issue", "%d assigned issues", issues_count) % issues_count }) do
= number_with_delimiter(issues_count)
- if header_link?(:merge_requests)
= nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter dropdown" }) do
@ -77,7 +77,7 @@
track_property: 'navigation',
container: 'body' } do
= sprite_icon('git-merge')
%span.badge.badge-pill.merge-requests-count.js-merge-requests-count{ class: ('hidden' if user_merge_requests_counts[:total] == 0) }
= gl_badge_tag({ size: :sm, variant: :warning }, { class: "js-merge-requests-count gl-ml-n2#{(' gl-display-none' if user_merge_requests_counts[:total] == 0)}", "aria-label": n_("%d merge request", "%d merge requests", user_merge_requests_counts[:total]) % user_merge_requests_counts[:total] }) do
= number_with_delimiter(user_merge_requests_counts[:total])
= sprite_icon('chevron-down', css_class: 'caret-down gl-mx-0!')
.dropdown-menu.dropdown-menu-right
@ -87,12 +87,12 @@
%li
= link_to assigned_mrs_dashboard_path, class: 'gl-display-flex! gl-align-items-center js-prefetch-document' do
= _('Assigned to you')
%span.badge.gl-badge.badge-pill.badge-muted.merge-request-badge.gl-ml-auto.js-assigned-mr-count{ class: "" }
= gl_badge_tag({ variant: :neutral, size: :sm }, { class: "js-assigned-mr-count gl-ml-auto" }) do
= user_merge_requests_counts[:assigned]
%li
= link_to reviewer_mrs_dashboard_path, class: 'gl-display-flex! gl-align-items-center js-prefetch-document' do
= _('Review requests for you')
%span.badge.gl-badge.badge-pill.badge-muted.merge-request-badge.gl-ml-auto.js-reviewer-mr-count{ class: "" }
= gl_badge_tag({ variant: :neutral, size: :sm }, { class: "js-reviewer-mr-count gl-ml-auto" }) do
= user_merge_requests_counts[:review_requested]
- if header_link?(:todos)
= nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do
@ -103,7 +103,9 @@
track_property: 'navigation',
container: 'body' } do
= sprite_icon('todo-done')
%span.badge.badge-pill.todos-count.js-todos-count{ class: ('hidden' if todos_pending_count == 0) }
-# The todos' counter badge's visibility is being toggled by adding or removing the .hidden class in Js.
-# We'll eventually migrate to .gl-display-none: https://gitlab.com/gitlab-org/gitlab/-/issues/351792.
= gl_badge_tag({ size: :sm, variant: :info }, { class: "js-todos-count gl-ml-n2#{(' hidden' if todos_pending_count == 0)}", "aria-label": _("Todos count") }) do
= todos_count_format(todos_pending_count)
%li.nav-item.header-help.dropdown.d-none.d-md-block{ **tracking_attrs('main_navigation', 'click_question_mark_link', 'navigation') }
= link_to help_path, class: 'header-help-dropdown-toggle gl-relative', data: { toggle: "dropdown" } do

View File

@ -0,0 +1,8 @@
---
name: dast_sharded_cloned_ci_builds
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78164
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351980
milestone: '14.8'
type: development
group: group::dynamic analysis
default_enabled: true

View File

@ -35,8 +35,12 @@
- 1
- - analytics_usage_trends_counter_job
- 1
- - app_sec_dast_scanner_profiles_builds_consistency
- 1
- - app_sec_dast_scans_consistency
- 1
- - app_sec_dast_site_profiles_builds_consistency
- 1
- - approval_rules_external_approval_rule_payload
- 1
- - approve_blocked_pending_approval_users

View File

@ -3,7 +3,7 @@
SCALABILITY_REVIEW_MESSAGE = <<~MSG
## Sidekiq queue changes
This merge request contains changes to Sidekiq queues. Please follow the [documentation on changing a queue's urgency](https://docs.gitlab.com/ee/development/sidekiq_style_guide.html#changing-a-queues-urgency).
This merge request contains changes to Sidekiq queues. Please follow the [documentation on changing a queue's urgency](https://docs.gitlab.com/ee/development/sidekiq/worker_attributes.html#job-urgency).
MSG
ADDED_QUEUES_MESSAGE = <<~MSG

View File

@ -1,6 +1,6 @@
---
stage: Verify
group: Testing
group: Pipeline Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---

View File

@ -8810,6 +8810,8 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- |
| <a id="boardepicancestorsauthorusername"></a>`authorUsername` | [`String`](#string) | Filter epics by author. |
| <a id="boardepicancestorsconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter epics by given confidentiality. |
| <a id="boardepicancestorscreatedafter"></a>`createdAfter` | [`Time`](#time) | Epics created after this date. |
| <a id="boardepicancestorscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Epics created before this date. |
| <a id="boardepicancestorsenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. |
| <a id="boardepicancestorsiid"></a>`iid` | [`ID`](#id) | IID of the epic, e.g., "1". |
| <a id="boardepicancestorsiidstartswith"></a>`iidStartsWith` | [`String`](#string) | Filter epics by IID for autocomplete. |
@ -8826,6 +8828,8 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="boardepicancestorsstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. |
| <a id="boardepicancestorsstate"></a>`state` | [`EpicState`](#epicstate) | Filter epics by state. |
| <a id="boardepicancestorstimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
| <a id="boardepicancestorsupdatedafter"></a>`updatedAfter` | [`Time`](#time) | Epics updated after this date. |
| <a id="boardepicancestorsupdatedbefore"></a>`updatedBefore` | [`Time`](#time) | Epics updated before this date. |
##### `BoardEpic.children`
@ -8843,6 +8847,8 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- |
| <a id="boardepicchildrenauthorusername"></a>`authorUsername` | [`String`](#string) | Filter epics by author. |
| <a id="boardepicchildrenconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter epics by given confidentiality. |
| <a id="boardepicchildrencreatedafter"></a>`createdAfter` | [`Time`](#time) | Epics created after this date. |
| <a id="boardepicchildrencreatedbefore"></a>`createdBefore` | [`Time`](#time) | Epics created before this date. |
| <a id="boardepicchildrenenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. |
| <a id="boardepicchildreniid"></a>`iid` | [`ID`](#id) | IID of the epic, e.g., "1". |
| <a id="boardepicchildreniidstartswith"></a>`iidStartsWith` | [`String`](#string) | Filter epics by IID for autocomplete. |
@ -8859,6 +8865,8 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="boardepicchildrenstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. |
| <a id="boardepicchildrenstate"></a>`state` | [`EpicState`](#epicstate) | Filter epics by state. |
| <a id="boardepicchildrentimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
| <a id="boardepicchildrenupdatedafter"></a>`updatedAfter` | [`Time`](#time) | Epics updated after this date. |
| <a id="boardepicchildrenupdatedbefore"></a>`updatedBefore` | [`Time`](#time) | Epics updated before this date. |
##### `BoardEpic.currentUserTodos`
@ -10326,6 +10334,8 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- |
| <a id="epicancestorsauthorusername"></a>`authorUsername` | [`String`](#string) | Filter epics by author. |
| <a id="epicancestorsconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter epics by given confidentiality. |
| <a id="epicancestorscreatedafter"></a>`createdAfter` | [`Time`](#time) | Epics created after this date. |
| <a id="epicancestorscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Epics created before this date. |
| <a id="epicancestorsenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. |
| <a id="epicancestorsiid"></a>`iid` | [`ID`](#id) | IID of the epic, e.g., "1". |
| <a id="epicancestorsiidstartswith"></a>`iidStartsWith` | [`String`](#string) | Filter epics by IID for autocomplete. |
@ -10342,6 +10352,8 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="epicancestorsstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. |
| <a id="epicancestorsstate"></a>`state` | [`EpicState`](#epicstate) | Filter epics by state. |
| <a id="epicancestorstimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
| <a id="epicancestorsupdatedafter"></a>`updatedAfter` | [`Time`](#time) | Epics updated after this date. |
| <a id="epicancestorsupdatedbefore"></a>`updatedBefore` | [`Time`](#time) | Epics updated before this date. |
##### `Epic.children`
@ -10359,6 +10371,8 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- |
| <a id="epicchildrenauthorusername"></a>`authorUsername` | [`String`](#string) | Filter epics by author. |
| <a id="epicchildrenconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter epics by given confidentiality. |
| <a id="epicchildrencreatedafter"></a>`createdAfter` | [`Time`](#time) | Epics created after this date. |
| <a id="epicchildrencreatedbefore"></a>`createdBefore` | [`Time`](#time) | Epics created before this date. |
| <a id="epicchildrenenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. |
| <a id="epicchildreniid"></a>`iid` | [`ID`](#id) | IID of the epic, e.g., "1". |
| <a id="epicchildreniidstartswith"></a>`iidStartsWith` | [`String`](#string) | Filter epics by IID for autocomplete. |
@ -10375,6 +10389,8 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="epicchildrenstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. |
| <a id="epicchildrenstate"></a>`state` | [`EpicState`](#epicstate) | Filter epics by state. |
| <a id="epicchildrentimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
| <a id="epicchildrenupdatedafter"></a>`updatedAfter` | [`Time`](#time) | Epics updated after this date. |
| <a id="epicchildrenupdatedbefore"></a>`updatedBefore` | [`Time`](#time) | Epics updated before this date. |
##### `Epic.currentUserTodos`
@ -11049,6 +11065,8 @@ Returns [`Epic`](#epic).
| ---- | ---- | ----------- |
| <a id="groupepicauthorusername"></a>`authorUsername` | [`String`](#string) | Filter epics by author. |
| <a id="groupepicconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter epics by given confidentiality. |
| <a id="groupepiccreatedafter"></a>`createdAfter` | [`Time`](#time) | Epics created after this date. |
| <a id="groupepiccreatedbefore"></a>`createdBefore` | [`Time`](#time) | Epics created before this date. |
| <a id="groupepicenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. |
| <a id="groupepiciid"></a>`iid` | [`ID`](#id) | IID of the epic, e.g., "1". |
| <a id="groupepiciidstartswith"></a>`iidStartsWith` | [`String`](#string) | Filter epics by IID for autocomplete. |
@ -11065,6 +11083,8 @@ Returns [`Epic`](#epic).
| <a id="groupepicstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. |
| <a id="groupepicstate"></a>`state` | [`EpicState`](#epicstate) | Filter epics by state. |
| <a id="groupepictimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
| <a id="groupepicupdatedafter"></a>`updatedAfter` | [`Time`](#time) | Epics updated after this date. |
| <a id="groupepicupdatedbefore"></a>`updatedBefore` | [`Time`](#time) | Epics updated before this date. |
##### `Group.epicBoard`
@ -11094,6 +11114,8 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- |
| <a id="groupepicsauthorusername"></a>`authorUsername` | [`String`](#string) | Filter epics by author. |
| <a id="groupepicsconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter epics by given confidentiality. |
| <a id="groupepicscreatedafter"></a>`createdAfter` | [`Time`](#time) | Epics created after this date. |
| <a id="groupepicscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Epics created before this date. |
| <a id="groupepicsenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. |
| <a id="groupepicsiid"></a>`iid` | [`ID`](#id) | IID of the epic, e.g., "1". |
| <a id="groupepicsiidstartswith"></a>`iidStartsWith` | [`String`](#string) | Filter epics by IID for autocomplete. |
@ -11110,6 +11132,8 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="groupepicsstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. |
| <a id="groupepicsstate"></a>`state` | [`EpicState`](#epicstate) | Filter epics by state. |
| <a id="groupepicstimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
| <a id="groupepicsupdatedafter"></a>`updatedAfter` | [`Time`](#time) | Epics updated after this date. |
| <a id="groupepicsupdatedbefore"></a>`updatedBefore` | [`Time`](#time) | Epics updated before this date. |
##### `Group.groupMembers`
@ -17041,12 +17065,16 @@ Roadmap sort values.
| Value | Description |
| ----- | ----------- |
| <a id="epicsortcreated_at_asc"></a>`CREATED_AT_ASC` | Sort by created_at by ascending order. |
| <a id="epicsortcreated_at_desc"></a>`CREATED_AT_DESC` | Sort by created_at by descending order. |
| <a id="epicsortend_date_asc"></a>`END_DATE_ASC` | Sort by end date in ascending order. |
| <a id="epicsortend_date_desc"></a>`END_DATE_DESC` | Sort by end date in descending order. |
| <a id="epicsortstart_date_asc"></a>`START_DATE_ASC` | Sort by start date in ascending order. |
| <a id="epicsortstart_date_desc"></a>`START_DATE_DESC` | Sort by start date in descending order. |
| <a id="epicsorttitle_asc"></a>`TITLE_ASC` | Sort by title in ascending order. |
| <a id="epicsorttitle_desc"></a>`TITLE_DESC` | Sort by title in descending order. |
| <a id="epicsortupdated_at_asc"></a>`UPDATED_AT_ASC` | Sort by updated_at by ascending order. |
| <a id="epicsortupdated_at_desc"></a>`UPDATED_AT_DESC` | Sort by updated_at by descending order. |
| <a id="epicsortend_date_asc"></a>`end_date_asc` **{warning-solid}** | **Deprecated** in 13.11. Use END_DATE_ASC. |
| <a id="epicsortend_date_desc"></a>`end_date_desc` **{warning-solid}** | **Deprecated** in 13.11. Use END_DATE_DESC. |
| <a id="epicsortstart_date_asc"></a>`start_date_asc` **{warning-solid}** | **Deprecated** in 13.11. Use START_DATE_ASC. |

View File

@ -1,6 +1,6 @@
---
stage: Verify
group: Testing
group: Pipeline Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---

View File

@ -243,8 +243,8 @@ curl --request POST \
}
```
The `ContentType` header must return a valid number. The maximum file size is 10 gigabytes.
The `ContentLength` header must be `application/gzip`.
The `Content-Length` header must return a valid number. The maximum file size is 10 gigabytes.
The `Content-Type` header must be `application/gzip`.
## Import status

View File

@ -1,8 +1,7 @@
---
stage: Verify
group: Testing
group: Pipeline Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference, api
---
# Visual Review discussions API **(PREMIUM)**

View File

@ -1,6 +1,6 @@
---
stage: Configure
group: Configure
stage: Verify
group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---

View File

@ -1,6 +1,6 @@
---
stage: Configure
group: Configure
stage: Verify
group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---

View File

@ -1,6 +1,6 @@
---
stage: Verify
group: Testing
group: Pipeline Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
author: Vincent Tunru
author_gitlab: Vinnl

View File

@ -1,8 +1,7 @@
---
stage: Verify
group: Testing
group: Pipeline Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference
---
# Metrics Reports **(PREMIUM)**

View File

@ -1,9 +1,8 @@
---
stage: Verify
group: Testing
group: Pipeline Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
disqus_identifier: 'https://docs.gitlab.com/ee/user/project/pipelines/job_artifacts.html'
type: reference, howto
---
# Job artifacts **(FREE)**

View File

@ -1,8 +1,7 @@
---
stage: Verify
group: Testing
group: Pipeline Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference, howto
---
# Pipeline artifacts **(FREE)**

View File

@ -1,8 +1,7 @@
---
stage: Verify
group: Testing
group: Pipeline Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference
---
# Review Apps **(FREE)**

View File

@ -1,8 +1,7 @@
---
stage: Verify
group: Testing
group: Pipeline Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference
---
# Unit test reports **(FREE)**

View File

@ -1,6 +1,6 @@
---
stage: Verify
group: Testing
group: Pipeline Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---

View File

@ -560,10 +560,6 @@ sudo -u git -H cp config/puma.rb.example config/puma.rb
# cores you have available. You can get that number via the `nproc` command.
sudo -u git -H editor config/puma.rb
# Configure Git global settings for git user
# 'autocrlf' is needed for the web editor
sudo -u git -H git config --global core.autocrlf input
# Disable 'git gc --auto' because GitLab already runs 'git gc' when needed
sudo -u git -H git config --global gc.auto 0

View File

@ -202,7 +202,7 @@ any Kubernetes-specific commands from the authorized project.
First, configure your Agent:
1. Go to you your Agent's configuration repository.
1. Go to your Agent's configuration repository.
1. Edit your Agent's `config.yaml` file authorizing the [project](#authorize-projects-to-use-an-agent) or [group](#authorize-groups-to-use-an-agent) you want to run Kubernetes commands from.
Then, configure the other project:
@ -231,6 +231,14 @@ To get the list of available contexts:
1. Open your terminal and connect to your cluster.
1. Run `kubectl config get-contexts`.
### `kubectl` commands not supported
The commands `kubectl exec`, `kubectl cp`, and `kubectl attach` are not supported by the CI/CD tunnel.
Anything else that uses the same API endpoints does not work either as they use the deprecated
SPDY protocol.
We [plan to add support for these features](https://gitlab.com/gitlab-org/gitlab/-/issues/346248)
in a future version of GitLab.
## Use impersonation to restrict project and group access **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345014) in GitLab 14.5.

View File

@ -1,7 +1,6 @@
---
type: reference
stage: Verify
group: Testing
group: Pipeline Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---

View File

@ -1,8 +1,7 @@
---
stage: Verify
group: Testing
group: Pipeline Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference, howto
---
# Accessibility testing **(FREE)**

View File

@ -1,8 +1,7 @@
---
stage: Verify
group: Testing
group: Pipeline Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference, howto
---
# Browser Performance Testing **(PREMIUM)**

View File

@ -1,8 +1,7 @@
---
stage: Verify
group: Testing
group: Pipeline Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference, howto
---
# Fail Fast Testing **(PREMIUM)**

View File

@ -1,8 +1,7 @@
---
stage: Verify
group: Testing
group: Pipeline Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference, howto
---
# Load Performance Testing **(PREMIUM)**

View File

@ -1,8 +1,7 @@
---
stage: Verify
group: Testing
group: Pipeline Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference, howto
---
# Test coverage visualization **(FREE)**

View File

@ -1,8 +1,7 @@
---
stage: Verify
group: Testing
group: Pipeline Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: index
description: "Test your code and display reports in merge requests"
---

View File

@ -23,7 +23,7 @@ module API
def retrieve_members(source, params:, deep: false)
members = deep ? find_all_members(source) : source_members(source).connected_to_user
members = members.includes(:user)
members = members.references(:user).merge(User.search(params[:query])) if params[:query].present?
members = members.references(:user).merge(User.search(params[:query], use_minimum_char_limit: false)) if params[:query].present?
members = members.where(user_id: params[:user_ids]) if params[:user_ids].present?
members
end

View File

@ -9,7 +9,17 @@ module Gitlab
Attribute = Struct.new(:name, :type)
LOG_KEY = Labkit::Context::LOG_KEY
KNOWN_KEYS = Labkit::Context::KNOWN_KEYS
KNOWN_KEYS = [
:user,
:project,
:root_namespace,
:client_id,
:caller_id,
:remote_ip,
:related_class,
:feature_category
].freeze
private_constant :KNOWN_KEYS
APPLICATION_ATTRIBUTES = [
Attribute.new(:project, Project),
@ -22,6 +32,10 @@ module Gitlab
Attribute.new(:feature_category, String)
].freeze
def self.known_keys
KNOWN_KEYS
end
def self.with_context(args, &block)
application_context = new(**args)
application_context.use(&block)

View File

@ -95,10 +95,6 @@ module Gitlab
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def git_bin_path
Gitlab.config.git.bin_path
end
def copy_files(source, destination)
# if we are copying files, create the destination folder
destination_folder = File.file?(source) ? File.dirname(destination) : destination

View File

@ -8,7 +8,7 @@ module Gitlab
InvalidQueueError = Class.new(StandardError)
WORKER_KEY = 'worker_class'
ALLOWED_KEYS = Gitlab::ApplicationContext::KNOWN_KEYS + [WORKER_KEY]
ALLOWED_KEYS = Gitlab::ApplicationContext.known_keys.map(&:to_s) + [WORKER_KEY]
attr_reader :queue_name
@ -53,7 +53,7 @@ module Gitlab
private
def transform_key(key)
if Gitlab::ApplicationContext::KNOWN_KEYS.include?(key)
if Gitlab::ApplicationContext.known_keys.include?(key.to_sym)
"meta.#{key}"
elsif key == WORKER_KEY
'class'

View File

@ -1,44 +0,0 @@
# frozen_string_literal: true
module SystemCheck
module App
class GitConfigCheck < SystemCheck::BaseCheck
OPTIONS = {
'core.autocrlf' => 'input'
}.freeze
set_name 'Git configured correctly?'
def check?
correct_options = OPTIONS.map do |name, value|
run_command(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value
end
correct_options.all?
end
# Tries to configure git itself
#
# Returns true if all subcommands were successful (according to their exit code)
# Returns false if any or all subcommands failed.
def repair!
return false unless gitlab_user?
command_success = OPTIONS.map do |name, value|
system(*%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value}))
end
command_success.all?
end
def show_error
try_fixing_it(
sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{OPTIONS['core.autocrlf']}\"")
)
for_more_information(
see_installation_guide_section('GitLab')
)
end
end
end
end

View File

@ -1,31 +0,0 @@
# frozen_string_literal: true
module SystemCheck
module App
class GitVersionCheck < SystemCheck::BaseCheck
set_name -> { "Git version >= #{self.required_version} ?" }
set_check_pass -> { "yes (#{self.current_version})" }
def self.required_version
@required_version ||= Gitlab::VersionInfo.parse('2.33.0')
end
def self.current_version
@current_version ||= Gitlab::VersionInfo.parse(Gitlab::TaskHelpers.run_command(%W(#{Gitlab.config.git.bin_path} --version)))
end
def check?
self.class.current_version.valid? && self.class.required_version <= self.class.current_version
end
def show_error
$stdout.puts "Your git bin path is \"#{Gitlab.config.git.bin_path}\""
try_fixing_it(
"Update your git to a version >= #{self.class.required_version} from #{self.class.current_version}"
)
fix_and_rerun
end
end
end
end

View File

@ -12,7 +12,6 @@ module SystemCheck
def self.checks
[
SystemCheck::App::GitConfigCheck,
SystemCheck::App::DatabaseConfigExistsCheck,
SystemCheck::App::MigrationsAreUpCheck,
SystemCheck::App::OrphanedGroupMembersCheck,
@ -28,7 +27,6 @@ module SystemCheck
SystemCheck::App::ProjectsHaveNamespaceCheck,
SystemCheck::App::RedisVersionCheck,
SystemCheck::App::RubyVersionCheck,
SystemCheck::App::GitVersionCheck,
SystemCheck::App::GitUserDefaultSSHConfigCheck,
SystemCheck::App::ActiveUsersCheck,
SystemCheck::App::AuthorizedKeysPermissionCheck,

View File

@ -22,8 +22,6 @@ namespace :gitlab do
proxies = Gitlab::Proxy.detect_proxy.map {|k, v| "#{k}: #{v}"}.join("\n\t\t")
end
# check Git version
git_version = run_and_match([Gitlab.config.git.bin_path, '--version'], /git version ([\d\.]+)/).to_a
# check Go version
go_version = run_and_match(%w(go version), /go version (.+)/).to_a
@ -43,7 +41,6 @@ namespace :gitlab do
puts "Bundler Version:#{bunder_version || "unknown".color(:red)}"
puts "Rake Version:\t#{rake_version || "unknown".color(:red)}"
puts "Redis Version:\t#{redis_version[1] || "unknown".color(:red)}"
puts "Git Version:\t#{git_version[1] || "unknown".color(:red)}"
puts "Sidekiq Version:#{Sidekiq::VERSION}"
puts "Go Version:\t#{go_version[1] || "unknown".color(:red)}"
@ -95,7 +92,6 @@ namespace :gitlab do
end
end
puts "GitLab Shell path:\t\t#{Gitlab.config.gitlab_shell.path}"
puts "Git:\t\t#{Gitlab.config.git.bin_path}"
end
end
end

View File

@ -146,6 +146,11 @@ msgid_plural "%d approvers (you've approved)"
msgstr[0] ""
msgstr[1] ""
msgid "%d assigned issue"
msgid_plural "%d assigned issues"
msgstr[0] ""
msgstr[1] ""
msgid "%d changed file"
msgid_plural "%d changed files"
msgstr[0] ""
@ -11382,9 +11387,6 @@ msgstr ""
msgid "Date merged"
msgstr ""
msgid "Date picker"
msgstr ""
msgid "Date range"
msgstr ""
@ -14123,9 +14125,6 @@ msgstr ""
msgid "Error occurred while updating the issue status"
msgstr ""
msgid "Error occurred while updating the issue weight"
msgstr ""
msgid "Error occurred. A blocked user cannot be deactivated"
msgstr ""
@ -22680,13 +22679,13 @@ msgstr ""
msgid "MergeRequests|Create issue to resolve thread"
msgstr ""
msgid "MergeRequests|Failed to squash. Should be done manually."
msgstr ""
msgid "MergeRequests|Saving the comment failed"
msgstr ""
msgid "MergeRequests|This project does not allow squashing commits when merge requests are accepted."
msgid "MergeRequests|Squashing failed: Squash the commits locally, resolve any conflicts, then push the branch."
msgstr ""
msgid "MergeRequests|Squashing not allowed: This project doesn't allow you to squash commits when merging."
msgstr ""
msgid "MergeRequests|Thread stays resolved"
@ -27082,6 +27081,18 @@ msgstr ""
msgid "Policy project doesn't exist"
msgstr ""
msgid "PolicyRuleMultiSelect|%{firstLabel} +%{numberOfAdditionalLabels} more"
msgstr ""
msgid "PolicyRuleMultiSelect|All %{itemTypeName}"
msgstr ""
msgid "PolicyRuleMultiSelect|Select %{itemTypeName}"
msgstr ""
msgid "PolicyRuleMultiSelect|Select all"
msgstr ""
msgid "Polling interval multiplier"
msgstr ""
@ -33402,12 +33413,6 @@ msgstr ""
msgid "Sidebar|None"
msgstr ""
msgid "Sidebar|Only numeral characters allowed"
msgstr ""
msgid "Sidebar|Weight"
msgstr ""
msgid "Sidekiq job compression threshold (bytes)"
msgstr ""
@ -37796,6 +37801,9 @@ msgstr ""
msgid "Today"
msgstr ""
msgid "Todos count"
msgstr ""
msgid "Todos|Are you looking for things to do? Take a look at %{strongStart}%{openIssuesLinkStart}open issues%{openIssuesLinkEnd}%{strongEnd}, contribute to %{strongStart}%{mergeRequestLinkStart}a merge request%{mergeRequestLinkEnd}%{mergeRequestLinkEnd}%{strongEnd}, or mention someone in a comment to automatically assign them a new to-do item."
msgstr ""

View File

@ -100,8 +100,7 @@ module QA
def exists?
api_get
# TODO: remove 'InternalServerError' from below line after https://gitlab.com/gitlab-org/gitlab/-/issues/349337 has been resolved
rescue ResourceNotFoundError, InternalServerError
rescue ResourceNotFoundError
false
end

View File

@ -1,11 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Manage', :requires_admin, quarantine: {
only: { subdomain: :staging },
type: :investigating,
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/350965'
} do
RSpec.describe 'Manage', :requires_admin do
describe 'Gitlab migration' do
let(:import_wait_duration) { { max_duration: 300, sleep_interval: 2 } }
let(:admin_api_client) { Runtime::API::Client.as_admin }

View File

@ -3,11 +3,7 @@
require_relative 'gitlab_project_migration_common'
module QA
RSpec.describe 'Manage', :requires_admin, quarantine: {
only: { subdomain: :staging },
type: :investigating,
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/351596'
} do
RSpec.describe 'Manage' do
describe 'Gitlab migration' do
include_context 'with gitlab project migration'

View File

@ -3,7 +3,7 @@
require_relative 'gitlab_project_migration_common'
module QA
RSpec.describe 'Manage', :requires_admin do
RSpec.describe 'Manage' do
describe 'Gitlab migration' do
include_context 'with gitlab project migration'

View File

@ -3,7 +3,7 @@
require_relative 'gitlab_project_migration_common'
module QA
RSpec.describe 'Manage', :requires_admin do
RSpec.describe 'Manage' do
describe 'Gitlab migration' do
include_context 'with gitlab project migration'

View File

@ -1,7 +1,9 @@
# frozen_string_literal: true
module QA
RSpec.shared_context 'with gitlab project migration', quarantine: {
# Disable on staging until bulk_import_projects toggle is on by default
# Otherwise tests running in parallel can disable feature in the middle of other test
RSpec.shared_context 'with gitlab project migration', :requires_admin, except: { subdomain: :staging }, quarantine: {
only: { job: 'praefect' },
type: :investigating,
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/348999'

View File

@ -64,7 +64,9 @@ module QA
Page::Profile::Accounts::Show.perform do |show|
show.delete_account(user.password)
end
Support::Waiter.wait_until { !user.exists? }
# TODO: Remove retry_on_exception once https://gitlab.com/gitlab-org/gitlab/-/issues/24294 is resolved
Support::Waiter.wait_until(retry_on_exception: true, sleep_interval: 3) { !user.exists? }
end
it 'allows recreating with same credentials', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347868' do

View File

@ -17,7 +17,7 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching d
it 'reflects dashboard issues count' do
visit issues_path
expect_counters('issues', '1')
expect_counters('issues', '1', n_("%d assigned issue", "%d assigned issues", 1) % 1)
issue.assignees = []
@ -26,14 +26,14 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching d
travel_to(3.minutes.from_now) do
visit issues_path
expect_counters('issues', '0')
expect_counters('issues', '0', n_("%d assigned issue", "%d assigned issues", 0) % 0)
end
end
it 'reflects dashboard merge requests count' do
visit merge_requests_path
expect_counters('merge_requests', '1')
expect_counters('merge_requests', '1', n_("%d merge request", "%d merge requests", 1) % 1)
merge_request.update!(assignees: [])
@ -42,7 +42,7 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching d
travel_to(3.minutes.from_now) do
visit merge_requests_path
expect_counters('merge_requests', '0')
expect_counters('merge_requests', '0', n_("%d merge request", "%d merge requests", 0) % 0)
end
end
@ -54,13 +54,14 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching d
merge_requests_dashboard_path(assignee_username: user.username)
end
def expect_counters(issuable_type, count)
def expect_counters(issuable_type, count, badge_label)
dashboard_count = find('.gl-tabs-nav li a.active')
nav_count = find(".dashboard-shortcuts-#{issuable_type}")
header_count = find(".header-content .#{issuable_type.tr('_', '-')}-count")
expect(dashboard_count).to have_content(count)
expect(nav_count).to have_content(count)
expect(header_count).to have_content(count)
within("span[aria-label='#{badge_label}']") do
expect(page).to have_content(count)
end
end
end

View File

@ -112,7 +112,9 @@ RSpec.describe 'Dashboard Merge Requests' do
end
it 'includes assigned and reviewers in badge' do
expect(find('.merge-requests-count')).to have_content('3')
within("span[aria-label='#{n_("%d merge request", "%d merge requests", 3) % 3}']") do
expect(page).to have_content('3')
end
expect(find('.js-assigned-mr-count')).to have_content('2')
expect(find('.js-reviewer-mr-count')).to have_content('1')
end

View File

@ -19,13 +19,13 @@ RSpec.describe 'Manually create a todo item from issue', :js do
expect(page).to have_content 'Mark as done'
end
page.within '.header-content .todos-count' do
page.within ".header-content span[aria-label='#{_('Todos count')}']" do
expect(page).to have_content '1'
end
visit project_issue_path(project, issue)
page.within '.header-content .todos-count' do
page.within ".header-content span[aria-label='#{_('Todos count')}']" do
expect(page).to have_content '1'
end
end
@ -36,10 +36,10 @@ RSpec.describe 'Manually create a todo item from issue', :js do
click_button 'Mark as done'
end
expect(page).to have_selector('.todos-count', visible: false)
expect(page).to have_selector(".header-content span[aria-label='#{_('Todos count')}']", visible: false)
visit project_issue_path(project, issue)
expect(page).to have_selector('.todos-count', visible: false)
expect(page).to have_selector(".header-content span[aria-label='#{_('Todos count')}']", visible: false)
end
end

View File

@ -7,15 +7,16 @@ RSpec.describe Autocomplete::UsersFinder do
# https://gitlab.com/gitlab-org/gitlab/-/issues/21432
describe '#execute' do
let!(:user1) { create(:user, username: 'johndoe') }
let!(:user2) { create(:user, :blocked, username: 'notsorandom') }
let!(:external_user) { create(:user, :external) }
let!(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
let_it_be(:user1) { create(:user, name: 'zzzzzname', username: 'johndoe') }
let_it_be(:user2) { create(:user, :blocked, username: 'notsorandom') }
let_it_be(:external_user) { create(:user, :external) }
let_it_be(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
let(:current_user) { create(:user) }
let(:params) { {} }
let(:project) { nil }
let(:group) { nil }
let_it_be(:project) { nil }
let_it_be(:group) { nil }
subject { described_class.new(params: params, current_user: current_user, project: project, group: group).execute.to_a }
@ -26,7 +27,7 @@ RSpec.describe Autocomplete::UsersFinder do
end
context 'when project passed' do
let(:project) { create(:project) }
let_it_be(:project) { create(:project) }
it { is_expected.to match_array([project.first_owner]) }
@ -43,16 +44,36 @@ RSpec.describe Autocomplete::UsersFinder do
it { is_expected.to match_array([project.first_owner]) }
end
end
context 'searching with less than 3 characters' do
let(:params) { { search: 'zz' } }
before do
project.add_guest(user1)
end
it 'allows partial matches' do
expect(subject).to contain_exactly(user1)
end
end
end
context 'when group passed and project not passed' do
let(:group) { create(:group, :public) }
let_it_be(:group) { create(:group, :public) }
before do
before_all do
group.add_users([user1], GroupMember::DEVELOPER)
end
it { is_expected.to match_array([user1]) }
context 'searching with less than 3 characters' do
let(:params) { { search: 'zz' } }
it 'allows partial matches' do
expect(subject).to contain_exactly(user1)
end
end
end
context 'when passed a subgroup' do
@ -76,6 +97,14 @@ RSpec.describe Autocomplete::UsersFinder do
let(:params) { { search: 'johndoe' } }
it { is_expected.to match_array([user1]) }
context 'searching with less than 3 characters' do
let(:params) { { search: 'zz' } }
it 'does not allow partial matches' do
expect(subject).to be_empty
end
end
end
context 'when filtered by skip_users' do

View File

@ -1,152 +1,140 @@
{
"type": "object",
"properties" : {
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": "integer" },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"merged_by": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": "integer" },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"merged_by": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"merge_user": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
"additionalProperties": false
},
"merge_user": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"merged_at": { "type": ["string", "null"] },
"closed_by": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
"additionalProperties": false
},
"merged_at": { "type": ["string", "null"] },
"closed_by": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"closed_at": { "type": ["string", "null"], "format": "date-time" },
"created_at": { "type": "string", "format": "date-time" },
"updated_at": { "type": "string", "format": "date-time" },
"target_branch": { "type": "string" },
"source_branch": { "type": "string" },
"upvotes": { "type": "integer" },
"downvotes": { "type": "integer" },
"author": {
"type": "object",
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
"additionalProperties": false
},
"closed_at": { "type": ["string", "null"], "format": "date-time" },
"created_at": { "type": "string", "format": "date-time" },
"updated_at": { "type": "string", "format": "date-time" },
"target_branch": { "type": "string" },
"source_branch": { "type": "string" },
"upvotes": { "type": "integer" },
"downvotes": { "type": "integer" },
"author": {
"type": "object",
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"assignee": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
"additionalProperties": false
},
"assignee": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"assignees": {
"items": {
"$ref": "./merge_request.json"
}
},
"source_project_id": { "type": "integer" },
"target_project_id": { "type": "integer" },
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"work_in_progress": { "type": "boolean" },
"milestone": {
"type": ["object", "null"],
"properties": {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": ["integer", "null"] },
"group_id": { "type": ["integer", "null"] },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"created_at": { "type": "string", "format": "date-time" },
"updated_at": { "type": "string", "format": "date-time" },
"due_date": { "type": "string", "format": "date-time" },
"start_date": { "type": "string", "format": "date-time" }
},
"additionalProperties": false
},
"merge_when_pipeline_succeeds": { "type": "boolean" },
"merge_status": { "type": "string" },
"sha": { "type": "string" },
"merge_commit_sha": { "type": ["string", "null"] },
"user_notes_count": { "type": "integer" },
"changes_count": { "type": "string" },
"should_remove_source_branch": { "type": ["boolean", "null"] },
"force_remove_source_branch": { "type": ["boolean", "null"] },
"discussion_locked": { "type": ["boolean", "null"] },
"web_url": { "type": "uri" },
"squash": { "type": "boolean" },
"time_stats": {
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["string", "null"] },
"human_total_time_spent": { "type": ["string", "null"] }
},
"allow_collaboration": { "type": ["boolean", "null"] },
"allow_maintainer_to_push": { "type": ["boolean", "null"] },
"references": {
"short": {"type": "string"},
"relative": {"type": "string"},
"full": {"type": "string"}
"additionalProperties": false
},
"assignees": {
"type": "array",
"items": {
"$ref": "user/basic.json"
}
},
"required": [
"id", "iid", "project_id", "title", "description",
"state", "created_at", "updated_at", "target_branch",
"source_branch", "upvotes", "downvotes", "author",
"assignee", "source_project_id", "target_project_id",
"labels", "work_in_progress", "milestone", "merge_when_pipeline_succeeds",
"merge_status", "sha", "merge_commit_sha", "user_notes_count",
"should_remove_source_branch", "force_remove_source_branch",
"web_url", "squash"
],
"head_pipeline": {
"source_project_id": { "type": "integer" },
"target_project_id": { "type": "integer" },
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"work_in_progress": { "type": "boolean" },
"milestone": {
"oneOf": [
{ "type": "null" },
{ "$ref": "pipeline/detail.json" }
{ "$ref": "milestone.json" }
]
},
"merge_when_pipeline_succeeds": { "type": "boolean" },
"merge_status": { "type": "string" },
"sha": { "type": "string" },
"merge_commit_sha": { "type": ["string", "null"] },
"user_notes_count": { "type": "integer" },
"changes_count": { "type": "string" },
"should_remove_source_branch": { "type": ["boolean", "null"] },
"force_remove_source_branch": { "type": ["boolean", "null"] },
"discussion_locked": { "type": ["boolean", "null"] },
"web_url": { "type": "uri" },
"squash": { "type": "boolean" },
"time_stats": {
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["string", "null"] },
"human_total_time_spent": { "type": ["string", "null"] }
},
"allow_collaboration": { "type": ["boolean", "null"] },
"allow_maintainer_to_push": { "type": ["boolean", "null"] },
"references": {
"short": {"type": "string"},
"relative": {"type": "string"},
"full": {"type": "string"}
}
},
"required": [
"id", "iid", "project_id", "title", "description",
"state", "created_at", "updated_at", "target_branch",
"source_branch", "upvotes", "downvotes", "author",
"assignee", "source_project_id", "target_project_id",
"labels", "work_in_progress", "milestone", "merge_when_pipeline_succeeds",
"merge_status", "sha", "merge_commit_sha", "user_notes_count",
"should_remove_source_branch", "force_remove_source_branch",
"web_url", "squash"
],
"head_pipeline": {
"oneOf": [
{ "type": "null" },
{ "$ref": "pipeline/detail.json" }
]
}
}

View File

@ -1,35 +0,0 @@
import { GlDropdown } from '@gitlab/ui';
import { getByText } from '@testing-library/dom';
import { shallowMount } from '@vue/test-utils';
import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
describe('MultiSelectDropdown Component', () => {
it('renders items slot', () => {
const wrapper = shallowMount(MultiSelectDropdown, {
propsData: {
text: '',
headerText: '',
},
slots: {
items: '<p>Test</p>',
},
});
expect(getByText(wrapper.element, 'Test')).toBeDefined();
});
it('renders search slot', () => {
const wrapper = shallowMount(MultiSelectDropdown, {
propsData: {
text: '',
headerText: '',
},
slots: {
search: '<p>Search</p>',
},
stubs: {
GlDropdown,
},
});
expect(getByText(wrapper.element, 'Search')).toBeDefined();
});
});

View File

@ -1,41 +0,0 @@
import { GlDatepicker } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import datePicker from '~/vue_shared/components/pikaday.vue';
describe('datePicker', () => {
let wrapper;
const buildWrapper = (propsData = {}) => {
wrapper = shallowMount(datePicker, {
propsData,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('should emit newDateSelected when GlDatePicker emits the input event', () => {
const minDate = new Date();
const maxDate = new Date();
const selectedDate = new Date();
const theDate = selectedDate.toISOString().slice(0, 10);
buildWrapper({ minDate, maxDate, selectedDate });
expect(wrapper.find(GlDatepicker).props()).toMatchObject({
minDate,
maxDate,
value: selectedDate,
});
wrapper.find(GlDatepicker).vm.$emit('input', selectedDate);
expect(wrapper.emitted('newDateSelected')[0][0]).toBe(theDate);
});
it('should emit the hidePicker event when GlDatePicker emits the close event', () => {
buildWrapper();
wrapper.find(GlDatepicker).vm.$emit('close');
expect(wrapper.emitted('hidePicker')).toHaveLength(1);
});
});

View File

@ -1,69 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
describe('CollapsedCalendarIcon', () => {
let wrapper;
const defaultProps = {
containerClass: 'test-class',
text: 'text',
tooltipText: 'tooltip text',
showIcon: false,
};
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(CollapsedCalendarIcon, {
propsData: { ...defaultProps, ...props },
directives: {
GlTooltip: createMockDirective(),
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
const findGlIcon = () => wrapper.findComponent(GlIcon);
const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip');
it('adds class to container', () => {
expect(wrapper.classes()).toContain(defaultProps.containerClass);
});
it('does not render calendar icon when showIcon is false', () => {
expect(findGlIcon().exists()).toBe(false);
});
it('renders calendar icon when showIcon is true', () => {
createComponent({
props: { showIcon: true },
});
expect(findGlIcon().exists()).toBe(true);
});
it('renders text', () => {
expect(wrapper.text()).toBe(defaultProps.text);
});
it('renders tooltipText as tooltip', () => {
expect(getTooltip().value).toBe(defaultProps.tooltipText);
});
it('emits click event when container is clicked', async () => {
wrapper.trigger('click');
await nextTick();
expect(wrapper.emitted('click')[0]).toBeDefined();
});
});

View File

@ -1,126 +0,0 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import DatePicker from '~/vue_shared/components/pikaday.vue';
import SidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
describe('SidebarDatePicker', () => {
let wrapper;
const createComponent = (propsData = {}, data = {}) => {
wrapper = mount(SidebarDatePicker, {
propsData,
data: () => data,
});
};
afterEach(() => {
wrapper.destroy();
});
const findDatePicker = () => wrapper.findComponent(DatePicker);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findEditButton = () => wrapper.find('.title .btn-blank');
const findRemoveButton = () => wrapper.find('.value-content .btn-blank');
const findSidebarToggle = () => wrapper.find('.title .gutter-toggle');
const findValueContent = () => wrapper.find('.value-content');
it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => {
createComponent();
wrapper.find('.issuable-sidebar-header .gutter-toggle').trigger('click');
expect(wrapper.emitted('toggleCollapse')).toEqual([[]]);
});
it('should render collapsed-calendar-icon', () => {
createComponent();
expect(wrapper.find('.sidebar-collapsed-icon').exists()).toBe(true);
});
it('should render value when not editing', () => {
createComponent();
expect(findValueContent().exists()).toBe(true);
});
it('should render None if there is no selectedDate', () => {
createComponent();
expect(findValueContent().text()).toBe('None');
});
it('should render date-picker when editing', () => {
createComponent({}, { editing: true });
expect(findDatePicker().exists()).toBe(true);
});
it('should render label', () => {
const label = 'label';
createComponent({ label });
expect(wrapper.find('.title').text()).toBe(label);
});
it('should render loading-icon when isLoading', () => {
createComponent({ isLoading: true });
expect(findLoadingIcon().exists()).toBe(true);
});
describe('editable', () => {
beforeEach(() => {
createComponent({ editable: true });
});
it('should render edit button', () => {
expect(findEditButton().text()).toBe('Edit');
});
it('should enable editing when edit button is clicked', async () => {
findEditButton().trigger('click');
await nextTick();
expect(wrapper.vm.editing).toBe(true);
});
});
it('should render date if selectedDate', () => {
createComponent({ selectedDate: new Date('07/07/2017') });
expect(wrapper.find('.value-content strong').text()).toBe('Jul 7, 2017');
});
describe('selectedDate and editable', () => {
beforeEach(() => {
createComponent({ selectedDate: new Date('07/07/2017'), editable: true });
});
it('should render remove button if selectedDate and editable', () => {
expect(findRemoveButton().text()).toBe('remove');
});
it('should emit saveDate with null when remove button is clicked', () => {
findRemoveButton().trigger('click');
expect(wrapper.emitted('saveDate')).toEqual([[null]]);
});
});
describe('showToggleSidebar', () => {
beforeEach(() => {
createComponent({ showToggleSidebar: true });
});
it('should render toggle-sidebar when showToggleSidebar', () => {
expect(findSidebarToggle().exists()).toBe(true);
});
it('should emit toggleCollapse when toggle sidebar is clicked', () => {
findSidebarToggle().trigger('click');
expect(wrapper.emitted('toggleCollapse')).toEqual([[]]);
});
});
});

View File

@ -125,6 +125,17 @@ RSpec.describe Gitlab::ApplicationContext do
.to include(project: project.full_path, root_namespace: project.full_path_components.first)
end
it 'contains known keys' do
context = described_class.new(project: project)
# Make sure all possible keys would be included
allow(context).to receive_message_chain(:set_values, :include?).and_return(true)
# If a newly added key is added to the context hash, we need to list it in
# the known keys constant. This spec ensures that we do.
expect(context.to_lazy_hash.keys).to contain_exactly(*described_class.known_keys)
end
describe 'setting the client' do
let_it_be(:remote_ip) { '127.0.0.1' }
let_it_be(:runner) { create(:ci_runner) }

View File

@ -2612,6 +2612,12 @@ RSpec.describe User do
it 'returns users with a exact matching name shorter than 3 chars regardless of the casing' do
expect(described_class.search(user3.name.upcase)).to eq([user3])
end
context 'when use_minimum_char_limit is false' do
it 'returns users with a partially matching name' do
expect(described_class.search('u', use_minimum_char_limit: false)).to eq([user3, user, user2])
end
end
end
describe 'email matching' do
@ -2671,6 +2677,12 @@ RSpec.describe User do
it 'returns users with a exact matching username shorter than 3 chars regardless of the casing' do
expect(described_class.search(user3.username.upcase)).to eq([user3])
end
context 'when use_minimum_char_limit is false' do
it 'returns users with a partially matching username' do
expect(described_class.search('se', use_minimum_char_limit: false)).to eq([user3, user, user2])
end
end
end
it 'returns no matches for an empty string' do
@ -2682,196 +2694,6 @@ RSpec.describe User do
end
end
describe '.search_without_secondary_emails' do
let_it_be(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'someone.1@example.com' ) }
let_it_be(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'another.2@example.com' ) }
let_it_be(:email) { create(:email, user: another_user, email: 'alias@example.com') }
it 'returns users with a matching name' do
expect(described_class.search_without_secondary_emails(user.name)).to eq([user])
end
it 'returns users with a partially matching name' do
expect(described_class.search_without_secondary_emails(user.name[0..2])).to eq([user])
end
it 'returns users with a matching name regardless of the casing' do
expect(described_class.search_without_secondary_emails(user.name.upcase)).to eq([user])
end
it 'returns users with a matching email' do
expect(described_class.search_without_secondary_emails(user.email)).to eq([user])
end
it 'does not return users with a partially matching email' do
expect(described_class.search_without_secondary_emails(user.email[1...-1])).to be_empty
end
it 'returns users with a matching email regardless of the casing' do
expect(described_class.search_without_secondary_emails(user.email.upcase)).to eq([user])
end
it 'returns users with a matching username' do
expect(described_class.search_without_secondary_emails(user.username)).to eq([user])
end
it 'returns users with a partially matching username' do
expect(described_class.search_without_secondary_emails(user.username[0..2])).to eq([user])
end
it 'returns users with a matching username regardless of the casing' do
expect(described_class.search_without_secondary_emails(user.username.upcase)).to eq([user])
end
it 'does not return users with a matching whole secondary email' do
expect(described_class.search_without_secondary_emails(email.email)).not_to include(email.user)
end
it 'does not return users with a matching part of secondary email' do
expect(described_class.search_without_secondary_emails(email.email[1...-1])).to be_empty
end
it 'returns no matches for an empty string' do
expect(described_class.search_without_secondary_emails('')).to be_empty
end
it 'returns no matches for nil' do
expect(described_class.search_without_secondary_emails(nil)).to be_empty
end
end
describe '.search_with_public_emails' do
let_it_be(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'someone.1@example.com' ) }
let_it_be(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'another.2@example.com' ) }
let_it_be(:public_email) do
create(:email, :confirmed, user: another_user, email: 'alias@example.com').tap do |email|
another_user.update!(public_email: email.email)
end
end
let_it_be(:secondary_email) do
create(:email, :confirmed, user: another_user, email: 'secondary@example.com')
end
it 'returns users with a matching name' do
expect(described_class.search_with_public_emails(user.name)).to match_array([user])
end
it 'returns users with a partially matching name' do
expect(described_class.search_with_public_emails(user.name[0..2])).to match_array([user])
end
it 'returns users with a matching name regardless of the casing' do
expect(described_class.search_with_public_emails(user.name.upcase)).to match_array([user])
end
it 'returns users with a matching public email' do
expect(described_class.search_with_public_emails(another_user.public_email)).to match_array([another_user])
end
it 'does not return users with a partially matching email' do
expect(described_class.search_with_public_emails(another_user.public_email[1...-1])).to be_empty
end
it 'returns users with a matching email regardless of the casing' do
expect(described_class.search_with_public_emails(another_user.public_email.upcase)).to match_array([another_user])
end
it 'returns users with a matching username' do
expect(described_class.search_with_public_emails(user.username)).to match_array([user])
end
it 'returns users with a partially matching username' do
expect(described_class.search_with_public_emails(user.username[0..2])).to match_array([user])
end
it 'returns users with a matching username regardless of the casing' do
expect(described_class.search_with_public_emails(user.username.upcase)).to match_array([user])
end
it 'does not return users with a matching whole private email' do
expect(described_class.search_with_public_emails(user.email)).not_to include(user)
end
it 'does not return users with a matching whole private email' do
expect(described_class.search_with_public_emails(secondary_email.email)).to be_empty
end
it 'does not return users with a matching part of secondary email' do
expect(described_class.search_with_public_emails(secondary_email.email[1...-1])).to be_empty
end
it 'does not return users with a matching part of private email' do
expect(described_class.search_with_public_emails(user.email[1...-1])).to be_empty
end
it 'returns no matches for an empty string' do
expect(described_class.search_with_public_emails('')).to be_empty
end
it 'returns no matches for nil' do
expect(described_class.search_with_public_emails(nil)).to be_empty
end
end
describe '.search_with_secondary_emails' do
let_it_be(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'someone.1@example.com' ) }
let_it_be(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'another.2@example.com' ) }
let_it_be(:email) { create(:email, user: another_user, email: 'alias@example.com') }
it 'returns users with a matching name' do
expect(described_class.search_with_secondary_emails(user.name)).to eq([user])
end
it 'returns users with a partially matching name' do
expect(described_class.search_with_secondary_emails(user.name[0..2])).to eq([user])
end
it 'returns users with a matching name regardless of the casing' do
expect(described_class.search_with_secondary_emails(user.name.upcase)).to eq([user])
end
it 'returns users with a matching email' do
expect(described_class.search_with_secondary_emails(user.email)).to eq([user])
end
it 'does not return users with a partially matching email' do
expect(described_class.search_with_secondary_emails(user.email[1...-1])).to be_empty
end
it 'returns users with a matching email regardless of the casing' do
expect(described_class.search_with_secondary_emails(user.email.upcase)).to eq([user])
end
it 'returns users with a matching username' do
expect(described_class.search_with_secondary_emails(user.username)).to eq([user])
end
it 'returns users with a partially matching username' do
expect(described_class.search_with_secondary_emails(user.username[0..2])).to eq([user])
end
it 'returns users with a matching username regardless of the casing' do
expect(described_class.search_with_secondary_emails(user.username.upcase)).to eq([user])
end
it 'returns users with a matching whole secondary email' do
expect(described_class.search_with_secondary_emails(email.email)).to eq([email.user])
end
it 'does not return users with a matching part of secondary email' do
expect(described_class.search_with_secondary_emails(email.email[1...-1])).to be_empty
end
it 'returns no matches for an empty string' do
expect(described_class.search_with_secondary_emails('')).to be_empty
end
it 'returns no matches for nil' do
expect(described_class.search_with_secondary_emails(nil)).to be_empty
end
end
describe '.user_search_minimum_char_limit' do
it 'returns true' do
expect(described_class.user_search_minimum_char_limit).to be(true)

View File

@ -60,7 +60,8 @@ RSpec.describe Ci::RetryBuildService do
artifacts_file artifacts_metadata artifacts_size commands
resource resource_group_id processed security_scans author
pipeline_id report_results pending_state pages_deployments
queuing_entry runtime_metadata trace_metadata].freeze
queuing_entry runtime_metadata trace_metadata
dast_site_profile dast_scanner_profile].freeze
shared_examples 'build duplication' do
let_it_be(:another_pipeline) { create(:ci_empty_pipeline, project: project) }

View File

@ -388,7 +388,7 @@ RSpec.describe MergeRequests::MergeService do
end
it 'logs and saves error if there is an error when squashing' do
error_message = 'Failed to squash. Should be done manually'
error_message = 'Squashing failed: Squash the commits locally, resolve any conflicts, then push the branch.'
allow_any_instance_of(MergeRequests::SquashService).to receive(:squash!).and_return(nil)
merge_request.update!(squash: true)

View File

@ -82,7 +82,7 @@ RSpec.describe MergeRequests::RebaseService do
context 'with a pre-receive failure' do
let(:pre_receive_error) { "Commit message does not follow the pattern 'ACME'" }
let(:merge_error) { "Something went wrong during the rebase pre-receive hook: #{pre_receive_error}." }
let(:merge_error) { "The rebase pre-receive hook failed: #{pre_receive_error}." }
before do
allow(repository).to receive(:gitaly_operation_client).and_raise(Gitlab::Git::PreReceiveError, "GitLab: #{pre_receive_error}")

View File

@ -168,7 +168,7 @@ RSpec.describe MergeRequests::SquashService do
it 'raises a squash error' do
expect(service.execute).to match(
status: :error,
message: a_string_including('does not allow squashing commits when merge requests are accepted'))
message: a_string_including('allow you to squash commits when merging'))
end
end
@ -205,7 +205,7 @@ RSpec.describe MergeRequests::SquashService do
end
it 'returns an error' do
expect(service.execute).to match(status: :error, message: a_string_including('squash'))
expect(service.execute).to match(status: :error, message: a_string_including('Squash'))
end
end
end
@ -232,7 +232,7 @@ RSpec.describe MergeRequests::SquashService do
end
it 'returns an error' do
expect(service.execute).to match(status: :error, message: a_string_including('squash'))
expect(service.execute).to match(status: :error, message: a_string_including('Squash'))
end
it 'cleans up the temporary directory' do

View File

@ -36,12 +36,38 @@ end
RSpec::Matchers.define :match_response_schema do |schema, dir: nil, **options|
match do |response|
schema_path = Pathname.new(SchemaPath.expand(schema, dir))
validator = SchemaPath.validator(schema_path)
@schema_path = Pathname.new(SchemaPath.expand(schema, dir))
validator = SchemaPath.validator(@schema_path)
data = Gitlab::Json.parse(response.body)
@data = Gitlab::Json.parse(response.body)
validator.valid?(data)
@schema_errors = validator.validate(@data)
@schema_errors.none?
end
failure_message do |actual|
message = []
message << <<~MESSAGE
expected JSON response to match schema #{@schema_path.inspect}.
JSON input: #{Gitlab::Json.pretty_generate(@data).indent(2)}
Schema errors:
MESSAGE
@schema_errors.each do |error|
property_name, actual_value = error.values_at('data_pointer', 'data')
property_name = 'root' if property_name.empty?
message << <<~MESSAGE
Property: #{property_name}
Actual value: #{Gitlab::Json.pretty_generate(actual_value).indent(2)}
Error: #{JSONSchemer::Errors.pretty(error)}
MESSAGE
end
message.join("\n")
end
end

View File

@ -1,39 +0,0 @@
# frozen_string_literal: true
require 'rake_helper'
RSpec.describe 'gitlab:env:info', :silence_stdout do
before do
Rake.application.rake_require 'tasks/gitlab/info'
stub_warn_user_is_not_gitlab
allow(Gitlab::Popen).to receive(:popen)
end
describe 'git version' do
before do
allow(Gitlab::Popen).to receive(:popen).with([Gitlab.config.git.bin_path, '--version'])
.and_return(git_version)
end
context 'when git installed' do
let(:git_version) { 'git version 2.10.0' }
it 'prints git version' do
run_rake_task('gitlab:env:info')
expect($stdout.string).to match(/Git Version:(.*)2.10.0/)
end
end
context 'when git not installed' do
let(:git_version) { '' }
it 'prints unknown' do
run_rake_task('gitlab:env:info')
expect($stdout.string).to match(/Git Version:(.*)unknown/)
end
end
end
end