Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-01-17 15:16:12 +00:00
parent dd33e91737
commit 8432be20de
73 changed files with 2670 additions and 280 deletions

View file

@ -5,9 +5,6 @@ GraphQL/FieldDefinitions:
- app/graphql/types/group_type.rb
- app/graphql/types/issue_type.rb
- app/graphql/types/label_type.rb
- app/graphql/types/project_type.rb
- app/graphql/types/projects/topic_type.rb
- app/graphql/types/release_type.rb
- ee/app/graphql/types/ci/code_quality_degradation_type.rb
- ee/app/graphql/types/epic_type.rb
- ee/app/graphql/types/group_release_stats_type.rb

View file

@ -18,21 +18,6 @@ Rails/SaveBang:
- ee/spec/models/visible_approvable_spec.rb
- ee/spec/models/vulnerabilities/feedback_spec.rb
- ee/spec/models/vulnerabilities/issue_link_spec.rb
- ee/spec/services/geo/blob_verification_secondary_service_spec.rb
- ee/spec/services/geo/files_expire_service_spec.rb
- ee/spec/services/geo/metrics_update_service_spec.rb
- ee/spec/services/geo/registry_consistency_service_spec.rb
- ee/spec/services/geo/repository_verification_secondary_service_spec.rb
- ee/spec/services/groups/autocomplete_service_spec.rb
- ee/spec/services/ldap_group_reset_service_spec.rb
- ee/spec/services/lfs/unlock_file_service_spec.rb
- ee/spec/services/merge_trains/refresh_merge_request_service_spec.rb
- ee/spec/services/quick_actions/interpret_service_spec.rb
- ee/spec/services/slash_commands/global_slack_handler_spec.rb
- ee/spec/services/start_pull_mirroring_service_spec.rb
- ee/spec/services/status_page/trigger_publish_service_spec.rb
- ee/spec/services/todo_service_spec.rb
- ee/spec/services/vulnerability_feedback/create_service_spec.rb
- spec/lib/backup/manager_spec.rb
- spec/lib/gitlab/alerting/alert_spec.rb
- spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb

View file

@ -1 +1 @@
501a7a9b19eb80ec039caeb8019cab7a8cfcbb44
7ce0d18ad44686865aa0dbf5f1b47d9cc05988be

View file

@ -0,0 +1,3 @@
import { initWorkItemsHierarchy } from '~/work_items_hierarchy/work_items_hierarchy_bundle';
initWorkItemsHierarchy();

View file

@ -1,6 +1,6 @@
<script>
import { GlIcon, GlSprintf, GlLink, GlFormCheckbox, GlToggle } from '@gitlab/ui';
import { GlButton, GlIcon, GlSprintf, GlLink, GlFormCheckbox, GlToggle } from '@gitlab/ui';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
import { __, s__ } from '~/locale';
import {
@ -41,16 +41,19 @@ export default {
pucWarningHelpText: s__(
'ProjectSettings|Highlight the usage of hidden unicode characters. These have innocent uses for right-to-left languages, but can also be used in potential exploits.',
),
confirmButtonText: __('Save changes'),
},
components: {
projectFeatureSetting,
projectSettingRow,
GlButton,
GlIcon,
GlSprintf,
GlLink,
GlFormCheckbox,
GlToggle,
ConfirmDanger,
},
mixins: [settingsMixin],
@ -163,6 +166,15 @@ export default {
required: false,
default: '',
},
confirmationPhrase: {
type: String,
required: true,
},
showVisibilityConfirmModal: {
type: Boolean,
required: false,
default: false,
},
},
data() {
const defaults = {
@ -274,6 +286,12 @@ export default {
cveIdRequestIsDisabled() {
return this.visibilityLevel !== visibilityOptions.PUBLIC;
},
isVisibilityReduced() {
return (
this.showVisibilityConfirmModal &&
this.visibilityLevel < this.currentSettings.visibilityLevel
);
},
},
watch: {
@ -774,5 +792,24 @@ export default {
<template #help>{{ $options.i18n.pucWarningHelpText }}</template>
</gl-form-checkbox>
</project-setting-row>
<confirm-danger
v-if="isVisibilityReduced"
button-class="qa-visibility-features-permissions-save-button"
button-variant="confirm"
:disabled="false"
:phrase="confirmationPhrase"
:button-text="$options.i18n.confirmButtonText"
data-testid="project-features-save-button"
@confirm="$emit('confirm')"
/>
<gl-button
v-else
type="submit"
variant="confirm"
data-testid="project-features-save-button"
button-class="qa-visibility-features-permissions-save-button"
>
{{ $options.i18n.confirmButtonText }}
</gl-button>
</div>
</template>

View file

@ -1,4 +1,5 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import settingsPanel from './components/settings_panel.vue';
export default function initProjectPermissionsSettings() {
@ -6,8 +7,36 @@ export default function initProjectPermissionsSettings() {
const componentPropsEl = document.querySelector('.js-project-permissions-form-data');
const componentProps = JSON.parse(componentPropsEl.innerHTML);
const {
targetFormId,
additionalInformation,
confirmDangerMessage,
confirmButtonText,
showVisibilityConfirmModal,
htmlConfirmationMessage,
phrase: confirmationPhrase,
} = mountPoint.dataset;
return new Vue({
el: mountPoint,
render: (createElement) => createElement(settingsPanel, { props: { ...componentProps } }),
provide: {
additionalInformation,
confirmDangerMessage,
confirmButtonText,
htmlConfirmationMessage: parseBoolean(htmlConfirmationMessage),
},
render: (createElement) =>
createElement(settingsPanel, {
props: {
...componentProps,
confirmationPhrase,
showVisibilityConfirmModal: parseBoolean(showVisibilityConfirmModal),
},
on: {
confirm: () => {
if (targetFormId) document.getElementById(targetFormId)?.submit();
},
},
}),
});
}

View file

@ -1,5 +1,54 @@
import { __ } from '~/locale';
export const widgetTypes = {
title: 'TITLE',
};
export const WI_TITLE_TRACK_LABEL = 'item_title';
export const workItemTypes = {
EPIC: {
title: __('Epic'),
icon: 'epic',
color: '#694CC0',
backgroundColor: '#E1D8F9',
},
ISSUE: {
title: __('Issue'),
icon: 'issues',
color: '#1068BF',
backgroundColor: '#CBE2F9',
},
TASK: {
title: __('Task'),
icon: 'task-done',
color: '#217645',
backgroundColor: '#C3E6CD',
},
INCIDENT: {
title: __('Incident'),
icon: 'issue-type-incident',
backgroundColor: '#db2a0f',
color: '#FDD4CD',
iconSize: 16,
},
SUB_EPIC: {
title: __('Child epic'),
icon: 'epic',
color: '#AB6100',
backgroundColor: '#F5D9A8',
},
REQUIREMENT: {
title: __('Requirement'),
icon: 'requirements',
color: '#0068c5',
backgroundColor: '#c5e3fb',
},
TEST_CASE: {
title: __('Test case'),
icon: 'issue-type-test-case',
backgroundColor: '#007a3f',
color: '#bae8cb',
iconSize: 16,
},
};

View file

@ -0,0 +1,96 @@
<script>
import { GlBanner } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
import { workItemTypes } from '~/work_items/constants';
import RESPONSE from '../static_response';
import { WORK_ITEMS_SURVEY_COOKIE_NAME } from '../constants';
import Hierarchy from './hierarchy.vue';
export default {
components: {
GlBanner,
Hierarchy,
},
inject: ['illustrationPath', 'licensePlan'],
data() {
return {
bannerVisible: !parseBoolean(Cookies.get(WORK_ITEMS_SURVEY_COOKIE_NAME)),
workItemHierarchy: RESPONSE[this.licensePlan],
};
},
computed: {
hasUnavailableStructure() {
return this.workItemTypes.unavailable.length > 0;
},
workItemTypes() {
return this.workItemHierarchy.reduce(
(itemTypes, item) => {
const key = item.available ? 'available' : 'unavailable';
itemTypes[key].push({
...item,
...workItemTypes[item.type],
nestedTypes: item.nestedTypes
? item.nestedTypes.map((type) => workItemTypes[type])
: null,
});
return itemTypes;
},
{ available: [], unavailable: [] },
);
},
},
methods: {
handleClose() {
Cookies.set(WORK_ITEMS_SURVEY_COOKIE_NAME, 'true', { expires: 365 * 10 });
this.bannerVisible = false;
},
},
};
</script>
<template>
<div>
<gl-banner
v-if="bannerVisible"
class="gl-mt-4 gl-px-5!"
:title="s__('Hierarchy|Help us improve work items in GitLab!')"
:button-text="s__('Hierarchy|Take the work items survey')"
button-link="https://forms.gle/u1BmRp8rTbwj52iq5"
:svg-path="illustrationPath"
@close="handleClose"
>
<p>
{{
s__(
'Hierarchy|Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you.',
)
}}
</p>
</gl-banner>
<h3 class="gl-mt-5!">{{ s__('Hierarchy|Planning hierarchy') }}</h3>
<p>
{{
s__(
'Hierarchy|Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals.',
)
}}
</p>
<div class="gl-font-weight-bold gl-mb-2">{{ s__('Hierarchy|Current structure') }}</div>
<p class="gl-mb-3!">{{ s__('Hierarchy|You can start using these items now.') }}</p>
<hierarchy :work-item-types="workItemTypes.available" />
<div
v-if="hasUnavailableStructure"
data-testid="unavailable-structure"
class="gl-font-weight-bold gl-mt-5 gl-mb-2"
>
{{ s__('Hierarchy|Unavailable structure') }}
</div>
<p v-if="hasUnavailableStructure" class="gl-mb-3!">
{{ s__('Hierarchy|These items are unavailable in the current structure.') }}
</p>
<hierarchy :work-item-types="workItemTypes.unavailable" />
</div>
</template>

View file

@ -0,0 +1,119 @@
<script>
import { GlIcon, GlBadge } from '@gitlab/ui';
export default {
components: {
GlIcon,
GlBadge,
},
props: {
workItemTypes: {
type: Array,
required: true,
},
},
methods: {
isLastItem(index, workItem) {
const hasMoreThanOneItem = workItem.nestedTypes.length > 1;
const isLastItemInArray = index === workItem.nestedTypes.length - 1;
return isLastItemInArray && hasMoreThanOneItem;
},
nestedWorkItemTypeMargin(index, workItem) {
const isLastItemInArray = index === workItem.nestedTypes.length - 1;
const hasMoreThanOneItem = workItem.nestedTypes.length > 1;
if (isLastItemInArray && hasMoreThanOneItem) {
return 'gl-ml-0';
}
return 'gl-ml-6';
},
},
};
</script>
<template>
<div>
<div
v-for="workItem in workItemTypes"
:key="workItem.id"
class="gl-mb-3"
:class="{ flex: !workItem.available }"
>
<span
class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal"
data-testid="work-item-wrapper"
>
<span
:style="{
backgroundColor: workItem.backgroundColor,
color: workItem.color,
}"
class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper"
>
<gl-icon :size="workItem.iconSize || 12" :name="workItem.icon" />
</span>
{{ workItem.title }}
</span>
<gl-badge
v-if="!workItem.available"
variant="info"
icon="license"
size="sm"
class="gl-ml-3 gl-align-self-center"
>{{ workItem.license }}</gl-badge
>
<div v-if="workItem.nestedTypes" :class="{ 'gl-relative': workItem.nestedTypes.length > 1 }">
<svg
v-if="workItem.nestedTypes.length > 1"
class="hierarchy-rounded-arrow-tail gl-text-gray-400"
data-testid="hierarchy-rounded-arrow-tail"
width="2"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="0.75"
y1="1"
x2="0.75"
y2="100%"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
</svg>
<template v-for="(nestedWorkItem, index) in workItem.nestedTypes">
<div :key="nestedWorkItem.id" class="gl-display-block gl-mt-2 gl-ml-6">
<gl-icon name="arrow-down" class="gl-text-gray-400" />
</div>
<gl-icon
v-if="isLastItem(index, workItem)"
:key="nestedWorkItem.id"
name="level-up"
class="gl-text-gray-400 gl-ml-2 hierarchy-rounded-arrow"
/>
<span
:key="nestedWorkItem.id"
class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-mt-2 gl-line-height-normal"
:class="nestedWorkItemTypeMargin(index, workItem)"
>
<span
:style="{
backgroundColor: nestedWorkItem.backgroundColor,
color: nestedWorkItem.color,
}"
class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper"
>
<gl-icon :size="nestedWorkItem.iconSize || 12" :name="nestedWorkItem.icon" />
</span>
{{ nestedWorkItem.title }}
</span>
</template>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,12 @@
export const WORK_ITEMS_SURVEY_COOKIE_NAME = 'hide_work_items_hierarchy_survey';
/**
* Hard-coded strings since we're rendering hierarchy
* items from mock responses. Remove this when we
* have a real hierarchy endpoint.
*/
export const LICENSE_PLAN = {
FREE: 'free',
PREMIUM: 'premium',
ULTIMATE: 'ultimate',
};

View file

@ -0,0 +1,10 @@
import { LICENSE_PLAN } from './constants';
export function inferLicensePlan({ hasSubEpics, hasEpics }) {
if (hasSubEpics) {
return LICENSE_PLAN.ULTIMATE;
} else if (hasEpics) {
return LICENSE_PLAN.PREMIUM;
}
return LICENSE_PLAN.FREE;
}

View file

@ -0,0 +1,142 @@
const FREE_TIER = 'free';
const ULTIMATE_TIER = 'ultimate';
const PREMIUM_TIER = 'premium';
const RESPONSE = {
[FREE_TIER]: [
{
id: '1',
type: 'ISSUE',
available: true,
license: null,
nestedTypes: null,
},
{
id: '2',
type: 'TASK',
available: true,
license: null,
nestedTypes: null,
},
{
id: '3',
type: 'INCIDENT',
available: true,
license: null,
nestedTypes: null,
},
{
id: '4',
type: 'EPIC',
available: false,
license: 'Premium', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
{
id: '5',
type: 'SUB_EPIC',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
{
id: '6',
type: 'REQUIREMENT',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
{
id: '7',
type: 'TEST_CASE',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
],
[PREMIUM_TIER]: [
{
id: '1',
type: 'EPIC',
available: true,
license: null,
nestedTypes: ['ISSUE'],
},
{
id: '2',
type: 'TASK',
available: true,
license: null,
nestedTypes: null,
},
{
id: '3',
type: 'INCIDENT',
available: true,
license: null,
nestedTypes: null,
},
{
id: '5',
type: 'SUB_EPIC',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
{
id: '6',
type: 'REQUIREMENT',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
{
id: '7',
type: 'TEST_CASE',
available: false,
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
nestedTypes: null,
},
],
[ULTIMATE_TIER]: [
{
id: '1',
type: 'EPIC',
available: true,
license: null,
nestedTypes: ['SUB_EPIC', 'ISSUE'],
},
{
id: '2',
type: 'TASK',
available: true,
license: null,
nestedTypes: null,
},
{
id: '3',
type: 'INCIDENT',
available: true,
license: null,
nestedTypes: null,
},
{
id: '6',
type: 'REQUIREMENT',
available: true,
license: null,
nestedTypes: null,
},
{
id: '7',
type: 'TEST_CASE',
available: true,
license: null,
nestedTypes: null,
},
],
};
export default RESPONSE;

View file

@ -0,0 +1,26 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import App from './components/app.vue';
import { inferLicensePlan } from './hierarchy_util';
export const initWorkItemsHierarchy = () => {
const el = document.querySelector('#js-work-items-hierarchy');
const { illustrationPath, hasEpics, hasSubEpics } = el.dataset;
const licensePlan = inferLicensePlan({
hasEpics: parseBoolean(hasEpics),
hasSubEpics: parseBoolean(hasSubEpics),
});
return new Vue({
el,
provide: {
illustrationPath,
licensePlan,
},
render(createElement) {
return createElement(App);
},
});
};

View file

@ -32,3 +32,4 @@
@import './pages/storage_quota';
@import './pages/tree';
@import './pages/users';
@import './pages/hierarchy';

View file

@ -0,0 +1,15 @@
.hierarchy-rounded-arrow-tail {
position: absolute;
top: 4px;
left: 5px;
height: calc(100% - 20px);
}
.hierarchy-icon-wrapper {
height: $default-icon-size;
width: $default-icon-size;
}
.hierarchy-rounded-arrow {
transform: scale(1, -1) rotate(90deg);
}

View file

@ -772,6 +772,9 @@ svg {
.gl-mt-2 {
margin-top: 0.25rem;
}
.gl-mt-5 {
margin-top: 1rem;
}
.gl-mb-3 {
margin-bottom: 0.5rem;
}

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
module WorkItemsHierarchy
extend ActiveSupport::Concern
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def planning_hierarchy
return render_404 unless Feature.enabled?(:work_items_hierarchy, @project, default_enabled: :yaml)
render 'shared/planning_hierarchy'
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
WorkItemsHierarchy.prepend_mod_with('WorkItemsHierarchy')

View file

@ -9,6 +9,7 @@ class ProjectsController < Projects::ApplicationController
include RecordUserLastActivity
include ImportUrlParams
include FiltersEvents
include WorkItemsHierarchy
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
@ -52,6 +53,7 @@ class ProjectsController < Projects::ApplicationController
feature_category :team_planning, [:preview_markdown, :new_issuable_address]
feature_category :importers, [:export, :remove_export, :generate_new_export, :download_export]
feature_category :code_review, [:unfoldered_environment_names]
feature_category :portfolio_management, [:planning_hierarchy]
urgency :low, [:refs]
urgency :high, [:unfoldered_environment_names]

View file

@ -27,7 +27,6 @@ module Types
field :description, GraphQL::Types::String, null: true,
description: 'Short description of the project.'
markdown_field :description_html, null: true
field :tag_list, GraphQL::Types::String, null: true,
deprecated: { reason: 'Use `topics`', milestone: '13.12' },
@ -75,21 +74,6 @@ module Types
field :avatar_url, GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'URL to avatar image file of the project.'
{
issues: "Issues are",
merge_requests: "Merge Requests are",
wiki: 'Wikis are',
snippets: 'Snippets are',
container_registry: 'Container Registry is'
}.each do |feature, name_string|
field "#{feature}_enabled", GraphQL::Types::Boolean, null: true,
description: "Indicates if #{name_string} enabled for the current user"
define_method "#{feature}_enabled" do
object.feature_available?(feature, context[:current_user])
end
end
field :jobs_enabled, GraphQL::Types::Boolean, null: true,
description: 'Indicates if CI/CD pipeline jobs are enabled for the current user.'
@ -391,15 +375,6 @@ module Types
null: true,
description: 'Template used to create squash commit message in merge requests.'
def label(title:)
BatchLoader::GraphQL.for(title).batch(key: project) do |titles, loader, args|
LabelsFinder
.new(current_user, project: args[:key], title: titles)
.execute
.each { |label| loader.call(label.title, label) }
end
end
field :labels,
Types::LabelType.connection_type,
null: true,
@ -411,6 +386,32 @@ module Types
description: 'Work item types available to the project.',
feature_flag: :work_items
def label(title:)
BatchLoader::GraphQL.for(title).batch(key: project) do |titles, loader, args|
LabelsFinder
.new(current_user, project: args[:key], title: titles)
.execute
.each { |label| loader.call(label.title, label) }
end
end
{
issues: "Issues are",
merge_requests: "Merge Requests are",
wiki: 'Wikis are',
snippets: 'Snippets are',
container_registry: 'Container Registry is'
}.each do |feature, name_string|
field "#{feature}_enabled", GraphQL::Types::Boolean, null: true,
description: "Indicates if #{name_string} enabled for the current user"
define_method "#{feature}_enabled" do
object.feature_available?(feature, context[:current_user])
end
end
markdown_field :description_html, null: true
def avatar_url
object.avatar_url(only_path: false)
end

View file

@ -14,11 +14,12 @@ module Types
field :description, GraphQL::Types::String, null: true,
description: 'Description of the topic.'
markdown_field :description_html, null: true
field :avatar_url, GraphQL::Types::String, null: true,
description: 'URL to avatar image file of the topic.'
markdown_field :description_html, null: true
def avatar_url
object.avatar_url(only_path: false)
end

View file

@ -20,7 +20,6 @@ module Types
authorize: :download_code
field :description, GraphQL::Types::String, null: true,
description: 'Description (also known as "release notes") of the release.'
markdown_field :description_html, null: true
field :name, GraphQL::Types::String, null: true,
description: 'Name of the release.'
field :created_at, Types::TimeType, null: true,
@ -42,14 +41,16 @@ module Types
field :author, Types::UserType, null: true,
description: 'User that created the release.'
def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, release.author_id).find
end
field :commit, Types::CommitType, null: true,
complexity: 10, calls_gitaly: true,
description: 'Commit associated with the release.'
markdown_field :description_html, null: true
def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, release.author_id).find
end
def commit
return if release.sha.nil?

View file

@ -672,18 +672,16 @@ module ProjectsHelper
html_escape(message) % { strong_start: strong_start, strong_end: strong_end, project_name: project.name, group_name: project.group ? project.group.name : nil }
end
def visibility_confirm_modal_data(project, remove_form_id = nil)
def visibility_confirm_modal_data(project, target_form_id = nil)
{
remove_form_id: remove_form_id,
qa_selector: 'visibility_features_permissions_save_button',
button_text: _('Save changes'),
target_form_id: target_form_id,
button_testid: 'reduce-project-visibility-button',
button_variant: 'confirm',
confirm_button_text: _('Reduce project visibility'),
confirm_danger_message: confirm_reduce_visibility_message(project),
phrase: project.full_path,
additional_information: _('Note: current forks will keep their visibility level.'),
html_confirmation_message: true
html_confirmation_message: true.to_s,
show_visibility_confirm_modal: show_visibility_confirm_modal?(project).to_s
}
end

View file

@ -3,7 +3,7 @@
class ProjectCiCdSetting < ApplicationRecord
belongs_to :project, inverse_of: :ci_cd_settings
DEFAULT_GIT_DEPTH = 50
DEFAULT_GIT_DEPTH = 20
before_create :set_default_git_depth

View file

@ -240,6 +240,7 @@ class ProjectPolicy < BasePolicy
enable :read_wiki
enable :read_issue
enable :read_label
enable :read_work_items_hierarchy
enable :read_milestone
enable :read_snippet
enable :read_project_member
@ -572,6 +573,7 @@ class ProjectPolicy < BasePolicy
enable :read_issue_board_list
enable :read_wiki
enable :read_label
enable :read_work_items_hierarchy
enable :read_milestone
enable :read_snippet
enable :read_project_member

View file

@ -62,7 +62,7 @@
%div
- if show_recaptcha_sign_up?
= recaptcha_tags nonce: content_security_policy_nonce
.submit-container
.submit-container.gl-mt-5
= f.submit button_text, class: 'btn gl-button btn-confirm gl-display-block gl-w-full', data: { qa_selector: 'new_user_register_button' }
= render 'devise/shared/terms_of_service_notice', button_text: button_text
- if show_omniauth_providers && omniauth_providers_placement == :bottom

View file

@ -32,64 +32,5 @@
type_plural: type_plural,
active_tokens: @active_personal_access_tokens,
revoke_route_helper: ->(token) { revoke_profile_personal_access_token_path(token) }
- if Feature.enabled?(:hide_access_tokens, default_enabled: :yaml)
#js-tokens-app{ data: { tokens_data: tokens_app_data } }
- else
- unless Gitlab::CurrentSettings.disable_feed_token
.col-lg-12
%hr
.row.gl-mt-3.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= s_('AccessTokens|Feed token')
%p
= s_('AccessTokens|Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar. It is visible in those feed URLs.')
%p
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.feed-token-reset
= label_tag :feed_token, s_('AccessTokens|Feed token'), class: 'label-bold'
= text_field_tag :feed_token, current_user.feed_token, class: 'form-control gl-form-input js-select-on-focus', readonly: true
%p.form-text.text-muted
- reset_link = link_to s_('AccessTokens|reset this token'), [:reset, :feed_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.'), testid: :reset_feed_token_link }
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{link_reset_it}.') % { link_reset_it: reset_link }
= reset_message.html_safe
- if incoming_email_token_enabled?
.col-lg-12
%hr
.row.gl-mt-3.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= s_('AccessTokens|Incoming email token')
%p
= s_('AccessTokens|Your incoming email token authenticates you when you create a new issue by email, and is included in your personal project-specific email addresses.')
%p
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.incoming-email-token-reset
= label_tag :incoming_email_token, s_('AccessTokens|Incoming email token'), class: 'label-bold'
= text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control gl-form-input js-select-on-focus', readonly: true
%p.form-text.text-muted
- reset_link = link_to s_('AccessTokens|reset this token'), [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.'), testid: :reset_email_token_link }
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{link_reset_it}.') % { link_reset_it: reset_link }
= reset_message.html_safe
- if static_objects_external_storage_enabled?
.col-lg-12
%hr
.row.gl-mt-3.js-search-settings-section
.col-lg-4
%h4.gl-mt-0
= s_('AccessTokens|Static object token')
%p
= s_('AccessTokens|Your static object token authenticates you when repository static objects (such as archives or blobs) are served from an external storage.')
%p
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8
= label_tag :static_object_token, s_('AccessTokens|Static object token'), class: "label-bold"
= text_field_tag :static_object_token, current_user.static_object_token, class: 'form-control gl-form-input', readonly: true, onclick: 'this.select()'
%p.form-text.text-muted
- reset_link = url_for [:reset, :static_object_token, :profile]
- reset_link_start = '<a data-confirm="%{confirm}" rel="nofollow" data-method="put" href="%{url}">'.html_safe % { confirm: s_('AccessTokens|Are you sure?'), url: reset_link }
- reset_link_end = '</a>'.html_safe
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{reset_link_start}reset this token%{reset_link_end}.') % { reset_link_start: reset_link_start, reset_link_end: reset_link_end }
= reset_message.html_safe
#js-tokens-app{ data: { tokens_data: tokens_app_data } }

View file

@ -2,7 +2,7 @@
- page_title _("General")
- @content_class = "limit-container-width" unless fluid_layout
- expanded = expanded_by_default?
- remove_visibility_form_id = 'reduce-visibility-form'
- reduce_visibility_form_id = 'reduce-visibility-form'
%section.settings.general-settings.no-animate.expanded#js-general-settings
.settings-header
@ -18,11 +18,10 @@
%p= _('Choose visibility level, enable/disable project features and their permissions, disable email notifications, and show default award emoji.')
.settings-content
= form_for @project, html: { multipart: true, class: "sharing-permissions-form", id: remove_visibility_form_id }, authenticity_token: true do |f|
= form_for @project, html: { multipart: true, class: "sharing-permissions-form", id: reduce_visibility_form_id }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' }
%template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project).to_json.html_safe
.js-project-permissions-form
= f.submit _('Save changes'), class: "btn gl-button btn-confirm #{('js-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: visibility_confirm_modal_data(@project, remove_visibility_form_id)
.js-project-permissions-form{ data: visibility_confirm_modal_data(@project, reduce_visibility_form_id) }
%section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { qa_selector: 'merge_request_settings_content' } }
.settings-header

View file

@ -0,0 +1,5 @@
- page_title _("Planning hierarchy")
- has_sub_epics = Gitlab.ee? && @project&.feature_available?(:subepics)
- has_epics = Gitlab.ee? && @project&.feature_available?(:epics)
#js-work-items-hierarchy{ data: { has_sub_epics: has_sub_epics.to_s, has_epics: has_epics.to_s, illustration_path: image_path('illustrations/rocket-launch-md.svg') } }

View file

@ -1,22 +1,23 @@
- sslStatus = hook.enable_ssl_verification ? _('enabled') : _('disabled')
- sslBadgeText = _('SSL Verification:') + ' ' + sslStatus
%li
.row
.col-md-8.col-lg-7
%strong.light-header
= hook.url
- if hook.rate_limited?
%span.gl-badge.badge-danger.badge-pill.sm= _('Disabled')
= gl_badge_tag(_('Disabled'), variant: :danger, size: :sm)
- elsif hook.permanently_disabled?
%span.gl-badge.badge-danger.badge-pill.sm= s_('Webhooks|Failed to connect')
= gl_badge_tag(s_('Webhooks|Failed to connect'), variant: :danger, size: :sm)
- elsif hook.temporarily_disabled?
%span.gl-badge.badge-warning.badge-pill.sm= s_('Webhooks|Fails to connect')
= gl_badge_tag(s_('Webhooks|Fails to connect'), variant: :warning, size: :sm)
%div
- hook.class.triggers.each_value do |trigger|
- if hook.public_send(trigger)
%span.gl-badge.badge-muted.badge-pill.sm.gl-mt-2.deploy-project-label= trigger.to_s.titleize
%span.gl-badge.badge-muted.badge-pill.sm.gl-mt-2
= _('SSL Verification:')
= hook.enable_ssl_verification ? _('enabled') : _('disabled')
= gl_badge_tag(trigger.to_s.titleize, size: :sm)
= gl_badge_tag(sslBadgeText, size: :sm)
.col-md-4.col-lg-5.text-right-md.gl-mt-2
%span>= render 'shared/web_hooks/test_button', hook: hook, button_class: 'btn-sm btn-default gl-mr-3'

View file

@ -1,8 +1,8 @@
---
name: hide_access_tokens
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76280
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347490
milestone: '14.6'
name: work_items_hierarchy
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76720
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350451
milestone: '14.7'
type: development
group: group::access
default_enabled: true
group: group::product planning
default_enabled: false

View file

@ -641,6 +641,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :generate_new_export
get :download_export
get :activity
get :planning_hierarchy
get :refs
put :new_issuable_address
get :unfoldered_environment_names

View file

@ -1,8 +1,8 @@
- name: "Pseudonymizer" # The name of the feature to be deprecated
announcement_milestone: "14.7" # The milestone when this feature was first announced as deprecated.
announcement_date: "2021-01-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
announcement_date: "2022-01-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "15.0" # The milestone when this feature is planned to be removed
removal_date: "2021-05-22" # This should almost always be the 22nd of a month (YYYY-MM-22), the date of the milestone release when this feature is planned to be removed.
removal_date: "2022-05-22" # This should almost always be the 22nd of a month (YYYY-MM-22), the date of the milestone release when this feature is planned to be removed.
body: | # Do not modify this line, instead modify the lines below.
The Pseudonymizer feature is generally unused,
can cause production issues with large databases,

View file

@ -0,0 +1,30 @@
- name: "Sidekiq metrics and health checks configuration"
announcement_milestone: "14.7"
announcement_date: "2021-01-22"
removal_milestone: "15.0"
removal_date: "2022-05-22"
breaking_change: true
body: | # Do not modify this line, instead modify the lines below.
Exporting Sidekiq metrics and health checks using a single process and port is deprecated.
Support will be removed in 15.0.
We have updated Sidekiq to export [metrics and health checks from two separate processes](https://gitlab.com/groups/gitlab-org/-/epics/6409)
to improve stability and availability and prevent data loss in edge cases.
As those are two separate servers, a configuration change will be required in 15.0
to explicitly set separate ports for metrics and health-checks.
The newly introduced settings for `sidekiq['health_checks_*']`
should always be set in `gitlab.rb`.
For more information, check the documentation for [configuring Sidekiq](https://docs.gitlab.com/ee/administration/sidekiq.html).
These changes also require updates in either Prometheus to scrape the new endpoint or k8s health-checks to target the new
health-check port to work properly, otherwise either metrics or health-checks will disappear.
For the deprecation period those settings are optional
and GitLab will default the Sidekiq health-checks port to the same port as `sidekiq_exporter`
and only run one server (not changing the current behaviour).
Only if they are both set and a different port is provided, a separate metrics server will spin up
to serve the Sidekiq metrics, similar to the way Sidekiq will behave in 15.0.
stage: Enablement
tiers: [Free, Premium, Ultimate]
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347509
documentation_url: https://docs.gitlab.com/ee/administration/sidekiq.html

View file

@ -0,0 +1,28 @@
# frozen_string_literal: true
class AddIndexesForPrimaryEmailCleanupMigration < Gitlab::Database::Migration[1.0]
USERS_INDEX = :index_users_on_id_for_primary_email_migration
EMAIL_INDEX = :index_emails_on_email_user_id
disable_ddl_transaction!
def up
unless index_exists_by_name?(:users, USERS_INDEX)
disable_statement_timeout do
execute <<~SQL
CREATE INDEX CONCURRENTLY #{USERS_INDEX}
ON users (id) INCLUDE (email, confirmed_at)
WHERE confirmed_at IS NOT NULL
SQL
end
end
add_concurrent_index :emails, [:email, :user_id], name: EMAIL_INDEX
end
def down
remove_concurrent_index_by_name :users, USERS_INDEX
remove_concurrent_index_by_name :emails, EMAIL_INDEX
end
end

View file

@ -0,0 +1,59 @@
# frozen_string_literal: true
class CleanupAfterAddPrimaryEmailToEmailsIfUserConfirmed < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
MIGRATION_NAME = 'AddPrimaryEmailToEmailsIfUserConfirmed'
BATCH_SIZE = 10_000
# Stubbed class to access the User table
class User < ActiveRecord::Base
include ::EachBatch
self.table_name = 'users'
self.inheritance_column = :_type_disabled
scope :confirmed, -> { where.not(confirmed_at: nil) }
has_many :emails
end
# Stubbed class to access the Emails table
class Email < ActiveRecord::Base
self.table_name = 'emails'
self.inheritance_column = :_type_disabled
belongs_to :user
end
def up
finalize_background_migration(MIGRATION_NAME)
# Select confirmed users that do not have their primary email in the emails table,
# and create the email record. There should be none if the background migration
# completed, but in case there is any leftover, we deal with it synchronously.
not_exists_condition = 'NOT EXISTS (SELECT 1 FROM emails WHERE emails.email = users.email AND emails.user_id = users.id)'
User.confirmed.each_batch(of: BATCH_SIZE) do |user_batch|
user_batch.select(:id, :email, :confirmed_at).where(not_exists_condition).each do |user|
current_time = Time.now.utc
begin
Email.create(
user_id: user.id,
email: user.email,
confirmed_at: user.confirmed_at,
created_at: current_time,
updated_at: current_time
)
rescue StandardError => error
Gitlab::AppLogger.error("Could not add primary email #{user.email} to emails for user with ID #{user.id} due to #{error}")
end
end
end
end
def down
# Intentionally left blank
end
end

View file

@ -0,0 +1,28 @@
# frozen_string_literal: true
class DropTemporaryIndexesForPrimaryEmailMigration < Gitlab::Database::Migration[1.0]
USERS_INDEX = :index_users_on_id_for_primary_email_migration
EMAIL_INDEX = :index_emails_on_email_user_id
disable_ddl_transaction!
def up
remove_concurrent_index_by_name :users, USERS_INDEX
remove_concurrent_index_by_name :emails, EMAIL_INDEX
end
def down
unless index_exists_by_name?(:users, USERS_INDEX)
disable_statement_timeout do
execute <<~SQL
CREATE INDEX CONCURRENTLY #{USERS_INDEX}
ON users (id) INCLUDE (email, confirmed_at)
WHERE confirmed_at IS NOT NULL
SQL
end
end
add_concurrent_index :emails, [:email, :user_id], name: EMAIL_INDEX
end
end

View file

@ -0,0 +1 @@
fa4a39c3bea70d31e8144f8830ef0353f22a7a663a891d9043e79f362058fbde

View file

@ -0,0 +1 @@
529c7ea38bbaa0c29491c2dfdb654a4a6adba93122d9bc23d6632526ff7fdb05

View file

@ -0,0 +1 @@
34bfe07fff59a415540ca2c5c96b33dc9030c15b2ffbb30cb7deedeb939ae132

View file

@ -28,14 +28,14 @@ A logger emits a log message only if its log level is equal to or above the mini
The following log levels are supported:
| Level | Name |
|-------|---------|
| 0 | DEBUG |
| 1 | INFO |
| 2 | WARN |
| 3 | ERROR |
| 4 | FATAL |
| 5 | UNKNOWN |
| Level | Name |
|:------|:----------|
| 0 | `DEBUG` |
| 1 | `INFO` |
| 2 | `WARN` |
| 3 | `ERROR` |
| 4 | `FATAL` |
| 5 | `UNKNOWN` |
GitLab loggers emit all log messages because they are set to `DEBUG` by default.
@ -53,8 +53,8 @@ GITLAB_LOG_LEVEL=info
For some services, other log levels are in place that are not affected by this setting.
Some of these services have their own environment variables to override the log level. For example:
| Service | Log Level | Environment variable |
|----------------------|-----------|----------------------|
| Service | Log level | Environment variable |
|:---------------------|:----------|:---------------------|
| GitLab API | `INFO` | |
| GitLab Cleanup | `INFO` | `DEBUG` |
| GitLab Doctor | `INFO` | `VERBOSE` |
@ -84,26 +84,26 @@ are written to a file called `current`. The `logrotate` service built into GitLa
[manages all logs](https://docs.gitlab.com/omnibus/settings/logs.html#logrotate)
except those captured by `runit`.
| Log type | Managed by logrotate | Managed by svlogd/runit |
|-------------------------------------------------|------------------------|-------------------------|
| [Alertmanager Logs](#alertmanager-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
| [Crond Logs](#crond-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
| [Gitaly](#gitaly-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
| [GitLab Exporter for Omnibus](#gitlab-exporter) | **{dotted-circle}** No | **{check-circle}** Yes |
| [GitLab Pages Logs](#pages-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
| GitLab Rails | **{check-circle}** Yes | **{dotted-circle}** No |
| [GitLab Shell Logs](#gitlab-shelllog) | **{check-circle}** Yes | **{dotted-circle}** No |
| [Grafana Logs](#grafana-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
| [LogRotate Logs](#logrotate-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
| [Mailroom](#mail_room_jsonlog-default) | **{check-circle}** Yes | **{check-circle}** Yes |
| [NGINX](#nginx-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
| [PostgreSQL Logs](#postgresql-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
| [Praefect Logs](#praefect-logs) | **{dotted-circle}** Yes| **{check-circle}** Yes |
| [Prometheus Logs](#prometheus-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
| [Puma](#puma-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
| [Redis Logs](#redis-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
| [Registry Logs](#registry-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
| [Workhorse Logs](#workhorse-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
| Log type | Managed by logrotate | Managed by svlogd/runit |
|:------------------------------------------------|:------------------------|:------------------------|
| [Alertmanager Logs](#alertmanager-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
| [Crond Logs](#crond-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
| [Gitaly](#gitaly-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
| [GitLab Exporter for Omnibus](#gitlab-exporter) | **{dotted-circle}** No | **{check-circle}** Yes |
| [GitLab Pages Logs](#pages-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
| GitLab Rails | **{check-circle}** Yes | **{dotted-circle}** No |
| [GitLab Shell Logs](#gitlab-shelllog) | **{check-circle}** Yes | **{dotted-circle}** No |
| [Grafana Logs](#grafana-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
| [LogRotate Logs](#logrotate-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
| [Mailroom](#mail_room_jsonlog-default) | **{check-circle}** Yes | **{check-circle}** Yes |
| [NGINX](#nginx-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
| [PostgreSQL Logs](#postgresql-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
| [Praefect Logs](#praefect-logs) | **{dotted-circle}** Yes | **{check-circle}** Yes |
| [Prometheus Logs](#prometheus-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
| [Puma](#puma-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
| [Redis Logs](#redis-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
| [Registry Logs](#registry-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
| [Workhorse Logs](#workhorse-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
## `production_json.log`

View file

@ -27,13 +27,13 @@ lookup of authorized SSH keys.
WARNING:
OpenSSH version 6.9+ is required because `AuthorizedKeysCommand` must be
able to accept a fingerprint. Check the version of OpenSSH on your server.
able to accept a fingerprint. Check the version of OpenSSH on your server with `sshd -V`.
## Fast lookup is required for Geo **(PREMIUM)**
By default, GitLab manages an `authorized_keys` file that is located in the
`git` user's home directory. For most installations, this will be located under
`/var/opt/gitlab/.ssh/authorized_keys`, but you can use the following command to locate the `authorized_keys` on your system.:
`/var/opt/gitlab/.ssh/authorized_keys`, but you can use the following command to locate the `authorized_keys` on your system:
```shell
getent passwd git | cut -d: -f6 | awk '{print $1"/.ssh/authorized_keys"}'
@ -77,9 +77,13 @@ sudo service sshd reload
```
Confirm that SSH is working by commenting out your user's key in the `authorized_keys`
file (start the line with a `#` to comment it), and attempting to pull a repository.
file (start the line with a `#` to comment it), and from your local machine, attempt to pull a repository or run:
A successful pull would mean that GitLab was able to find the key in the database,
```shell
ssh -T git@gitlab.example.com
```
A successful pull or [welcome message](../../ssh/index.md#verify-that-you-can-connect) would mean that GitLab was able to find the key in the database,
since it is not present in the file anymore.
NOTE:
@ -114,7 +118,7 @@ adding a new one, and attempting to pull a repository.
Then you can backup and delete your `authorized_keys` file for best performance.
The current users' keys are already present in the database, so there is no need for migration
or for asking users to re-add their keys.
or for users to re-add their keys.
## How to go back to using the `authorized_keys` file

View file

@ -159,7 +159,8 @@ in the `.gitlab-ci.yml` file.
## Limit the number of changes fetched during clone
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/28919) in GitLab 12.0.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/28919) in GitLab 12.0.
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77576) `git depth` value in GitLab 14.7.
You can limit the number of changes that GitLab CI/CD fetches when it clones
a repository.
@ -171,8 +172,8 @@ a repository.
The maximum value is `1000`. To disable shallow clone and make GitLab CI/CD
fetch all branches and tags each time, keep the value empty or set to `0`.
In GitLab 12.0 and later, newly created projects automatically have a default
`git depth` value of `50`.
In GitLab versions 14.7 and later, newly created projects have a default `git depth`
value of `20`. GitLab versions 14.6 and earlier have a default `git depth` value of `50`.
This value can be overridden by the [`GIT_DEPTH` variable](../large_repositories/index.md#shallow-cloning)
in the `.gitlab-ci.yml` file.

View file

@ -335,7 +335,7 @@ can cause production issues with large databases,
and can interfere with object storage development.
It is now considered deprecated, and will be removed in GitLab 15.0.
Planned removal milestone: 15.0 (2021-05-22)
Planned removal milestone: 15.0 (2022-05-22)
### Removal of Static Site Editor
@ -352,6 +352,30 @@ only supported report file in 15.0, but this is the first step towards GitLab su
Planned removal milestone: 15.0 (2022-05-22)
### Sidekiq metrics and health checks configuration
Exporting Sidekiq metrics and health checks using a single process and port is deprecated.
Support will be removed in 15.0.
We have updated Sidekiq to export [metrics and health checks from two separate processes](https://gitlab.com/groups/gitlab-org/-/epics/6409)
to improve stability and availability and prevent data loss in edge cases.
As those are two separate servers, a configuration change will be required in 15.0
to explicitly set separate ports for metrics and health-checks.
The newly introduced settings for `sidekiq['health_checks_*']`
should always be set in `gitlab.rb`.
For more information, check the documentation for [configuring Sidekiq](https://docs.gitlab.com/ee/administration/sidekiq.html).
These changes also require updates in either Prometheus to scrape the new endpoint or k8s health-checks to target the new
health-check port to work properly, otherwise either metrics or health-checks will disappear.
For the deprecation period those settings are optional
and GitLab will default the Sidekiq health-checks port to the same port as `sidekiq_exporter`
and only run one server (not changing the current behaviour).
Only if they are both set and a different port is provided, a separate metrics server will spin up
to serve the Sidekiq metrics, similar to the way Sidekiq will behave in 15.0.
Planned removal milestone: 15.0 (2022-05-22)
### Tracing in GitLab
Tracing in GitLab is an integration with Jaeger, an open-source end-to-end distributed tracing system. GitLab users can navigate to their Jaeger instance to gain insight into the performance of a deployed application, tracking each function or microservice that handles a given request. Tracing in GitLab is deprecated in GitLab 14.7, and scheduled for removal in 15.0. To track work on a possible replacement, see the issue for [Opstrace integration with GitLab](https://gitlab.com/groups/gitlab-org/-/epics/6976).

View file

@ -537,6 +537,31 @@ See [Maintenance mode issue in GitLab 13.9 to 14.4](#maintenance-mode-issue-in-g
- See [Maintenance mode issue in GitLab 13.9 to 14.4](#maintenance-mode-issue-in-gitlab-139-to-144).
- For GitLab Enterprise Edition customers, we noticed an issue when [subscription expiration is upcoming, and you create new subgroups and projects](https://gitlab.com/gitlab-org/gitlab/-/issues/322546). If you fall under that category and get 500 errors, you can work around this issue:
1. SSH into you GitLab server, and open a Rails console:
```shell
sudo gitlab-rails console
```
1. Disable the following features:
```ruby
Feature.disable(:subscribable_subscription_banner)
Feature.disable(:subscribable_license_banner)
```
1. Restart Puma or Unicorn:
```shell
#For installations using Puma
sudo gitlab-ctl restart puma
#For installations using Unicorn
sudo gitlab-ctl restart unicorn
```
### 13.8.8
GitLab 13.8 includes a background migration to address [an issue with duplicate service records](https://gitlab.com/gitlab-org/gitlab/-/issues/290008). If duplicate services are present, this background migration must complete before a unique index is applied to the services table, which was [introduced in GitLab 13.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52563). Upgrades from GitLab 13.8 and earlier to later versions must include an intermediate upgrade to GitLab 13.8.8 and [must wait until the background migrations complete](#checking-for-background-migrations-before-upgrading) before proceeding.

View file

@ -63,6 +63,9 @@ vulnerability.
## Requirements
Dependency Scanning runs in the `test` stage, which is available by default. If you redefine the
stages in the `.gitlab-ci.yml` file, the `test` stage is required.
To run dependency scanning jobs, by default, you need GitLab Runner with the
[`docker`](https://docs.gitlab.com/runner/executors/docker.html) or
[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html) executor.

View file

@ -14,6 +14,8 @@ Currently, IaC scanning supports configuration files for Terraform, Ansible, AWS
## Requirements
IaC Scanning runs in the `test` stage, which is available by default. If you redefine the stages in the `.gitlab-ci.yml` file, the `test` stage is required.
To run IaC scanning jobs, by default, you need GitLab Runner with the
[`docker`](https://docs.gitlab.com/runner/executors/docker.html) or
[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html) executor.

View file

@ -370,10 +370,10 @@ For information on this, see the [general Application Security troubleshooting s
### Error: `Couldn't run the gitleaks command: exit status 2`
If a pipeline is triggered from a Merge Request containing 60 commits while the `GIT_DEPTH` variable
is set to 50 (a [project default](../../../ci/pipelines/settings.md#limit-the-number-of-changes-fetched-during-clone)),
the Secret Detection job fails as the clone is not deep enough to contain all of the
relevant commits.
If a pipeline is triggered from a Merge Request containing 60 commits while the `GIT_DEPTH` variable's
value is less than that, the Secret Detection job fails as the clone is not deep enough to contain all of the
relevant commits. For information on the current default value, see the
[pipeline configuration documentation](../../../ci/pipelines/settings.md#limit-the-number-of-changes-fetched-during-clone).
To confirm this as the cause of the error, set the
[logging level](../../application_security/secret_detection/index.md#logging-level) to `debug`, then

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View file

@ -20,6 +20,20 @@ To learn about hierarchies in general, common frameworks, and using GitLab for
portfolio management, see
[How to use GitLab for Agile portfolio planning and project management](https://about.gitlab.com/blog/2020/11/11/gitlab-for-agile-portfolio-planning-project-management/).
## View planning hierarchies
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340844/) in GitLab 14.7 and is behind the feature flag `work_items_hierarchy`.
To view the planning hierarchy in a project:
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Project information > Planning hierarchy**.
Under **Current structure**, you can see a hierarchy diagram that matches your current planning hierarchy.
The work items outside your subscription plan show up below **Unavailable structure**.
![Screenshot showing hierarchy page](img/view-project-work-item-hierarchy_v14_7.png)
## Hierarchies with epics
With epics, you can achieve the following hierarchy:

View file

@ -14,6 +14,8 @@ variables:
image: "$SECURE_ANALYZERS_PREFIX/secrets:$SECRETS_ANALYZER_VERSION"
services: []
allow_failure: true
variables:
GIT_DEPTH: "50"
# `rules` must be overridden explicitly by each child job
# see https://gitlab.com/gitlab-org/gitlab/-/issues/218444
artifacts:

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
# This module has the necessary methods to render
# work items hierarchy menu
module Sidebars
module Concerns
module WorkItemHierarchy
def hierarchy_menu_item(container, url, path)
unless show_hierarachy_menu_item?(container)
return ::Sidebars::NilMenuItem.new(item_id: :hierarchy)
end
::Sidebars::MenuItem.new(
title: _('Planning hierarchy'),
link: url,
active_routes: { path: path },
item_id: :hierarchy
)
end
def show_hierarachy_menu_item?(container)
Feature.enabled?(:work_items_hierarchy, container, default_enabled: :yaml) &&
can?(context.current_user, :read_work_items_hierarchy, container)
end
end
end
end

View file

@ -4,10 +4,13 @@ module Sidebars
module Projects
module Menus
class ProjectInformationMenu < ::Sidebars::Menu
include ::Sidebars::Concerns::WorkItemHierarchy
override :configure_menu_items
def configure_menu_items
add_item(activity_menu_item)
add_item(labels_menu_item)
add_item(hierarchy_menu_item(context.project, planning_hierarchy_project_path(context.project), 'projects#planning_hierarchy'))
add_item(members_menu_item)
true

View file

@ -1835,21 +1835,12 @@ msgstr ""
msgid "AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{linkStart}reset this token%{linkEnd}."
msgstr ""
msgid "AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{reset_link_start}reset this token%{reset_link_end}."
msgstr ""
msgid "AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{linkStart}reset this token%{linkEnd}."
msgstr ""
msgid "AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{link_reset_it}."
msgstr ""
msgid "AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{linkStart}reset this token%{linkEnd}."
msgstr ""
msgid "AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{link_reset_it}."
msgstr ""
msgid "AccessTokens|Personal Access Tokens"
msgstr ""
@ -1874,9 +1865,6 @@ msgstr ""
msgid "AccessTokens|Your static object token authenticates you when repository static objects (such as archives or blobs) are served from an external storage."
msgstr ""
msgid "AccessTokens|reset this token"
msgstr ""
msgid "AccessibilityReport|Learn more"
msgstr ""
@ -7083,6 +7071,9 @@ msgstr ""
msgid "Child"
msgstr ""
msgid "Child epic"
msgstr ""
msgid "Child epic does not exist."
msgstr ""
@ -17628,6 +17619,33 @@ msgstr[1] ""
msgid "Hide values"
msgstr ""
msgid "Hierarchy|Current structure"
msgstr ""
msgid "Hierarchy|Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals."
msgstr ""
msgid "Hierarchy|Help us improve work items in GitLab!"
msgstr ""
msgid "Hierarchy|Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you."
msgstr ""
msgid "Hierarchy|Planning hierarchy"
msgstr ""
msgid "Hierarchy|Take the work items survey"
msgstr ""
msgid "Hierarchy|These items are unavailable in the current structure."
msgstr ""
msgid "Hierarchy|Unavailable structure"
msgstr ""
msgid "Hierarchy|You can start using these items now."
msgstr ""
msgid "High or unknown vulnerabilities present"
msgstr ""
@ -26512,6 +26530,9 @@ msgstr ""
msgid "Plan:"
msgstr ""
msgid "Planning hierarchy"
msgstr ""
msgid "PlantUML"
msgstr ""
@ -30235,6 +30256,9 @@ msgstr ""
msgid "Required only if you are not using role instance credentials."
msgstr ""
msgid "Requirement"
msgstr ""
msgid "Requirement %{reference} has been added"
msgstr ""
@ -34918,6 +34942,9 @@ msgstr ""
msgid "Target-Branch"
msgstr ""
msgid "Task"
msgstr ""
msgid "Task ID: %{elastic_task}"
msgstr ""
@ -35163,6 +35190,9 @@ msgstr ""
msgid "Test Cases"
msgstr ""
msgid "Test case"
msgstr ""
msgid "Test coverage parsing"
msgstr ""

View file

@ -5,12 +5,9 @@ module QA
module Project
module Settings
class VisibilityFeaturesPermissions < Page::Base
view 'app/helpers/projects_helper.rb' do
element :visibility_features_permissions_save_button
end
view 'app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue' do
element :project_visibility_dropdown
element :visibility_features_permissions_save_button
end
def set_project_visibility(visibility)

View file

@ -2,7 +2,11 @@
module QA
RSpec.describe 'Create' do
describe 'Merge request creation from fork' do
describe 'Merge request creation from fork', quarantine: {
only: { subdomain: %i[canary production] },
issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/343801",
type: :investigation
} do
let(:merge_request) do
Resource::MergeRequestFromFork.fabricate_via_browser_ui! do |merge_request|
merge_request.fork_branch = 'feature-branch'

View file

@ -0,0 +1,39 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItemsHierarchy do
controller(ApplicationController) do
include WorkItemsHierarchy
end
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
render_views
before do
sign_in user
routes.draw { get :planning_hierarchy, to: "anonymous#planning_hierarchy" }
controller.instance_variable_set(:@project, project)
end
it 'renders hierarchy' do
stub_feature_flags(work_items_hierarchy: true)
get :planning_hierarchy
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to match(/id="js-work-items-hierarchy"/)
end
it 'renders 404' do
stub_feature_flags(work_items_hierarchy: false)
get :planning_hierarchy
expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).not_to match(/id="js-work-items-hierarchy"/)
end
end

View file

@ -62,66 +62,33 @@ RSpec.describe 'Profile account page', :js do
end
end
describe 'when I reset feed token' do
it 'resets feed token with `hide_access_tokens` feature flag enabled' do
visit profile_personal_access_tokens_path
it 'allows resetting of feed token' do
visit profile_personal_access_tokens_path
within('[data-testid="feed-token-container"]') do
previous_token = find_field('Feed token').value
within('[data-testid="feed-token-container"]') do
previous_token = find_field('Feed token').value
accept_confirm { click_link('reset this token') }
accept_confirm { click_link('reset this token') }
click_button('Click to reveal')
click_button('Click to reveal')
expect(find_field('Feed token').value).not_to eq(previous_token)
end
end
it 'resets feed token with `hide_access_tokens` feature flag disabled' do
stub_feature_flags(hide_access_tokens: false)
visit profile_personal_access_tokens_path
within('.feed-token-reset') do
previous_token = find("#feed_token").value
accept_confirm { find('[data-testid="reset_feed_token_link"]').click }
expect(find('#feed_token').value).not_to eq(previous_token)
end
expect(find_field('Feed token').value).not_to eq(previous_token)
end
end
describe 'when I reset incoming email token' do
before do
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
stub_feature_flags(bootstrap_confirmation_modals: false)
end
it 'allows resetting of incoming email token' do
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
it 'resets incoming email token with `hide_access_tokens` feature flag enabled' do
visit profile_personal_access_tokens_path
visit profile_personal_access_tokens_path
within('[data-testid="incoming-email-token-container"]') do
previous_token = find_field('Incoming email token').value
within('[data-testid="incoming-email-token-container"]') do
previous_token = find_field('Incoming email token').value
accept_confirm { click_link('reset this token') }
accept_confirm { click_link('reset this token') }
click_button('Click to reveal')
click_button('Click to reveal')
expect(find_field('Incoming email token').value).not_to eq(previous_token)
end
end
it 'resets incoming email token with `hide_access_tokens` feature flag disabled' do
stub_feature_flags(hide_access_tokens: false)
visit profile_personal_access_tokens_path
within('.incoming-email-token-reset') do
previous_token = find('#incoming_email_token').value
accept_confirm { find('[data-testid="reset_email_token_link"]').click }
expect(find('#incoming_email_token').value).not_to eq(previous_token)
end
expect(find_field('Incoming email token').value).not_to eq(previous_token)
end
end

View file

@ -132,7 +132,7 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
describe "feed token" do
context "when enabled" do
it "displays feed token with `hide_access_tokens` feature flag enabled" do
it "displays feed token" do
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false)
visit profile_personal_access_tokens_path
@ -143,15 +143,6 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
expect(page).to have_content(feed_token_description)
end
end
it "displays feed token with `hide_access_tokens` feature flag disabled" do
stub_feature_flags(hide_access_tokens: false)
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false)
visit profile_personal_access_tokens_path
expect(page).to have_field('Feed token', with: user.feed_token)
expect(page).to have_content(feed_token_description)
end
end
context "when disabled" do

View file

@ -24,7 +24,7 @@ RSpec.describe 'Edit Project Settings' do
# disable by clicking toggle
toggle_feature_off("project[project_feature_attributes][#{tool_name}_access_level]")
page.within('.sharing-permissions') do
find('input[value="Save changes"]').click
find('[data-testid="project-features-save-button"]').click
end
wait_for_requests
expect(page).not_to have_selector(".shortcuts-#{shortcut_name}")
@ -32,7 +32,7 @@ RSpec.describe 'Edit Project Settings' do
# re-enable by clicking toggle again
toggle_feature_on("project[project_feature_attributes][#{tool_name}_access_level]")
page.within('.sharing-permissions') do
find('input[value="Save changes"]').click
find('[data-testid="project-features-save-button"]').click
end
wait_for_requests
expect(page).to have_selector(".shortcuts-#{shortcut_name}")

View file

@ -47,7 +47,7 @@ RSpec.describe 'Projects settings' do
# disable by clicking toggle
forking_enabled_button.click
page.within('.sharing-permissions') do
find('input[value="Save changes"]').click
find('[data-testid="project-features-save-button"]').click
end
wait_for_requests
@ -77,7 +77,7 @@ RSpec.describe 'Projects settings' do
expect(default_award_emojis_input.value).to eq('false')
page.within('.sharing-permissions') do
find('input[value="Save changes"]').click
find('[data-testid="project-features-save-button"]').click
end
wait_for_requests

View file

@ -54,7 +54,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
find('input[value="Save changes"]').send_keys(:return)
find('[data-testid="project-features-save-button"]').send_keys(:return)
end
expect(page).not_to have_content 'Pipelines must succeed'
@ -74,7 +74,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .gl-toggle').click
find('input[value="Save changes"]').send_keys(:return)
find('[data-testid="project-features-save-button"]').send_keys(:return)
end
expect(page).to have_content 'Pipelines must succeed'
@ -95,7 +95,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
find('input[value="Save changes"]').send_keys(:return)
find('[data-testid="project-features-save-button"]').send_keys(:return)
end
expect(page).to have_content 'Pipelines must succeed'

View file

@ -5,14 +5,6 @@ require 'spec_helper'
RSpec.describe 'User changes public project visibility', :js do
include ProjectForksHelper
before do
fork_project(project, project.owner)
sign_in(project.owner)
visit edit_project_path(project)
end
shared_examples 'changing visibility to private' do
it 'requires confirmation' do
visibility_select = first('.project-feature-controls .select-control')
@ -34,15 +26,85 @@ RSpec.describe 'User changes public project visibility', :js do
end
end
context 'when a project is public' do
shared_examples 'does not require confirmation' do
it 'saves without confirmation' do
visibility_select = first('.project-feature-controls .select-control')
visibility_select.select('Private')
page.within('#js-shared-permissions') do
click_button 'Save changes'
end
wait_for_requests
expect(project.reload).to be_private
end
end
context 'when the project has forks' do
before do
fork_project(project, project.owner)
sign_in(project.owner)
visit edit_project_path(project)
end
context 'when a project is public' do
let(:project) { create(:project, :empty_repo, :public) }
it_behaves_like 'changing visibility to private'
end
context 'when the project is internal' do
let(:project) { create(:project, :empty_repo, :internal) }
it_behaves_like 'changing visibility to private'
end
context 'when the visibility level is untouched' do
let(:project) { create(:project, :empty_repo, :public) }
it 'saves without confirmation' do
expect(page).to have_selector('.js-emails-disabled', visible: true)
find('.js-emails-disabled input[type="checkbox"]').click
page.within('#js-shared-permissions') do
click_button 'Save changes'
end
wait_for_requests
expect(project.reload).to be_public
end
end
end
context 'when the project is not forked' do
let(:project) { create(:project, :empty_repo, :public) }
it_behaves_like 'changing visibility to private'
before do
sign_in(project.owner)
visit edit_project_path(project)
end
it_behaves_like 'does not require confirmation'
end
context 'when the project is internal' do
let(:project) { create(:project, :empty_repo, :internal) }
context 'with unlink_fork_network_upon_visibility_decrease = false' do
let(:project) { create(:project, :empty_repo, :public) }
it_behaves_like 'changing visibility to private'
before do
stub_feature_flags(unlink_fork_network_upon_visibility_decrease: false)
fork_project(project, project.owner)
sign_in(project.owner)
visit edit_project_path(project)
end
it_behaves_like 'does not require confirmation'
end
end

View file

@ -7,6 +7,7 @@ import {
visibilityLevelDescriptions,
visibilityOptions,
} from '~/pages/projects/shared/permissions/constants';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
const defaultProps = {
currentSettings: {
@ -47,6 +48,8 @@ const defaultProps = {
packagesAvailable: false,
packagesHelpPath: '/help/user/packages/index',
requestCveAvailable: true,
confirmationPhrase: 'my-fake-project',
showVisibilityConfirmModal: false,
};
describe('Settings Panel', () => {
@ -104,6 +107,7 @@ describe('Settings Panel', () => {
);
const findMetricsVisibilitySettings = () => wrapper.find({ ref: 'metrics-visibility-settings' });
const findOperationsSettings = () => wrapper.find({ ref: 'operations-settings' });
const findConfirmDangerButton = () => wrapper.findComponent(ConfirmDanger);
afterEach(() => {
wrapper.destroy();
@ -177,6 +181,44 @@ describe('Settings Panel', () => {
expect(findRequestAccessEnabledInput().exists()).toBe(false);
});
it('does not require confirmation if the visibility is reduced', async () => {
wrapper = mountComponent({
currentSettings: { visibilityLevel: visibilityOptions.INTERNAL },
});
expect(findConfirmDangerButton().exists()).toBe(false);
await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
expect(findConfirmDangerButton().exists()).toBe(false);
});
describe('showVisibilityConfirmModal=true', () => {
beforeEach(() => {
wrapper = mountComponent({
currentSettings: { visibilityLevel: visibilityOptions.INTERNAL },
showVisibilityConfirmModal: true,
});
});
it('will render the confirmation dialog if the visibility is reduced', async () => {
expect(findConfirmDangerButton().exists()).toBe(false);
await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
expect(findConfirmDangerButton().exists()).toBe(true);
});
it('emits the `confirm` event when the reduce visibility warning is confirmed', async () => {
expect(wrapper.emitted('confirm')).toBeUndefined();
await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
await findConfirmDangerButton().vm.$emit('confirm');
expect(wrapper.emitted('confirm')).toHaveLength(1);
});
});
});
describe('Issues settings', () => {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,78 @@
import { nextTick } from 'vue';
import { createLocalVue, mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { GlBanner } from '@gitlab/ui';
import App from '~/work_items_hierarchy/components/app.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('WorkItemsHierarchy App', () => {
let wrapper;
const createComponent = (props = {}, data = {}) => {
wrapper = extendedWrapper(
mount(App, {
localVue,
provide: {
illustrationPath: '/foo.svg',
licensePlan: 'free',
...props,
},
data() {
return data;
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
describe.each`
licensePlan
${'free'}
${'premium'}
${'ultimate'}
`('when licensePlan is $licensePlan', ({ licensePlan }) => {
beforeEach(() => {
createComponent({ licensePlan });
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
describe('survey banner', () => {
it('shows when the banner is visible', () => {
createComponent({}, { bannerVisible: true });
expect(wrapper.find(GlBanner).exists()).toBe(true);
});
it('hide when close is called', async () => {
createComponent({}, { bannerVisible: true });
wrapper.findByTestId('close-icon').trigger('click');
await nextTick();
expect(wrapper.find(GlBanner).exists()).toBe(false);
});
});
describe('Unavailable structure', () => {
it.each`
licensePlan | visible
${'free'} | ${true}
${'premium'} | ${true}
${'ultimate'} | ${false}
`('visibility is $visible when plan is $licensePlan', ({ licensePlan, visible }) => {
createComponent({ licensePlan });
expect(wrapper.findByTestId('unavailable-structure').exists()).toBe(visible);
});
});
});

View file

@ -0,0 +1,118 @@
import { createLocalVue, mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { GlBadge } from '@gitlab/ui';
import Hierarchy from '~/work_items_hierarchy/components/hierarchy.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import RESPONSE from '~/work_items_hierarchy/static_response';
import { workItemTypes } from '~/work_items/constants';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('WorkItemsHierarchy Hierarchy', () => {
let wrapper;
const workItemsFromResponse = (response) => {
return response.reduce(
(itemTypes, item) => {
const key = item.available ? 'available' : 'unavailable';
itemTypes[key].push({
...item,
...workItemTypes[item.type],
nestedTypes: item.nestedTypes
? item.nestedTypes.map((type) => workItemTypes[type])
: null,
});
return itemTypes;
},
{ available: [], unavailable: [] },
);
};
const createComponent = (props = {}) => {
wrapper = extendedWrapper(
mount(Hierarchy, {
localVue,
propsData: {
workItemTypes: props.workItemTypes,
...props,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
describe('available structure', () => {
let items = [];
beforeEach(() => {
items = workItemsFromResponse(RESPONSE.ultimate).available;
createComponent({ workItemTypes: items });
});
it('renders all work items', () => {
expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length);
});
it('does not render badges', () => {
expect(wrapper.find(GlBadge).exists()).toBe(false);
});
});
describe('unavailable structure', () => {
let items = [];
beforeEach(() => {
items = workItemsFromResponse(RESPONSE.premium).unavailable;
createComponent({ workItemTypes: items });
});
it('renders all work items', () => {
expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length);
});
it('renders license badges for all work items', () => {
expect(wrapper.findAll(GlBadge)).toHaveLength(items.length);
});
it('does not render svg icon for linking', () => {
expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe(false);
expect(wrapper.findByTestId('level-up-icon').exists()).toBe(false);
});
});
describe('nested work items', () => {
describe.each`
licensePlan | arrowTailVisible | levelUpIconVisible | arrowDownIconVisible
${'ultimate'} | ${true} | ${true} | ${true}
${'premium'} | ${false} | ${false} | ${true}
${'free'} | ${false} | ${false} | ${false}
`(
'when $licensePlan license',
({ licensePlan, arrowTailVisible, levelUpIconVisible, arrowDownIconVisible }) => {
let items = [];
beforeEach(() => {
items = workItemsFromResponse(RESPONSE[licensePlan]).available;
createComponent({ workItemTypes: items });
});
it(`${arrowTailVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe(
arrowTailVisible,
);
});
it(`${levelUpIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
expect(wrapper.findByTestId('level-up-icon').exists()).toBe(levelUpIconVisible);
});
it(`${arrowDownIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
expect(wrapper.findByTestId('arrow-down-icon').exists()).toBe(arrowDownIconVisible);
});
},
);
});
});

View file

@ -0,0 +1,16 @@
import { inferLicensePlan } from '~/work_items_hierarchy/hierarchy_util';
import { LICENSE_PLAN } from '~/work_items_hierarchy/constants';
describe('inferLicensePlan', () => {
it.each`
epics | subEpics | licensePlan
${true} | ${true} | ${LICENSE_PLAN.ULTIMATE}
${true} | ${false} | ${LICENSE_PLAN.PREMIUM}
${false} | ${false} | ${LICENSE_PLAN.FREE}
`(
'returns $licensePlan when epic is $epics and sub-epic is $subEpics',
({ epics, subEpics, licensePlan }) => {
expect(inferLicensePlan({ hasEpics: epics, hasSubEpics: subEpics })).toBe(licensePlan);
},
);
});

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::Concerns::WorkItemHierarchy do
shared_examples 'hierarchy menu' do
let(:item_id) { :hierarchy }
context 'when the feature is disabled does not render' do
before do
stub_feature_flags(work_items_hierarchy: false)
end
specify { is_expected.to be_nil }
end
context 'when the feature is enabled does render' do
before do
stub_feature_flags(work_items_hierarchy: true)
end
specify { is_expected.not_to be_nil }
end
end
describe 'Project hierarchy menu item' do
let_it_be_with_reload(:project) { create(:project, :repository) }
let(:user) { project.owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
subject { Sidebars::Projects::Menus::ProjectInformationMenu.new(context).renderable_items.index { |e| e.item_id == item_id } }
it_behaves_like 'hierarchy menu'
end
end

View file

@ -59,5 +59,25 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
specify { is_expected.to be_nil }
end
end
describe 'Hierarchy' do
let(:item_id) { :hierarchy }
context 'when the feature is disabled' do
before do
stub_feature_flags(work_items_hierarchy: false)
end
specify { is_expected.to be_nil }
end
context 'when the feature is enabled' do
before do
stub_feature_flags(work_items_hierarchy: true)
end
specify { is_expected.not_to be_nil }
end
end
end
end

View file

@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe CleanupAfterAddPrimaryEmailToEmailsIfUserConfirmed, :sidekiq do
let(:migration) { described_class.new }
let(:users) { table(:users) }
let(:emails) { table(:emails) }
let!(:user_1) { users.create!(name: 'confirmed-user-1', email: 'confirmed-1@example.com', confirmed_at: 3.days.ago, projects_limit: 100) }
let!(:user_2) { users.create!(name: 'confirmed-user-2', email: 'confirmed-2@example.com', confirmed_at: 1.day.ago, projects_limit: 100) }
let!(:user_3) { users.create!(name: 'confirmed-user-3', email: 'confirmed-3@example.com', confirmed_at: 1.day.ago, projects_limit: 100) }
let!(:user_4) { users.create!(name: 'unconfirmed-user', email: 'unconfirmed@example.com', confirmed_at: nil, projects_limit: 100) }
let!(:email_1) { emails.create!(email: 'confirmed-1@example.com', user_id: user_1.id, confirmed_at: 1.day.ago) }
let!(:email_2) { emails.create!(email: 'other_2@example.com', user_id: user_2.id, confirmed_at: 1.day.ago) }
before do
stub_const("#{described_class.name}::BATCH_SIZE", 2)
end
it 'consume any pending background migration job' do
expect_next_instance_of(Gitlab::BackgroundMigration::JobCoordinator) do |coordinator|
expect(coordinator).to receive(:steal).with('AddPrimaryEmailToEmailsIfUserConfirmed').twice
end
migration.up
end
it 'adds the primary email to emails for leftover confirmed users that do not have their primary email in the emails table', :aggregate_failures do
original_email_1_confirmed_at = email_1.reload.confirmed_at
expect { migration.up }.to change { emails.count }.by(2)
expect(emails.find_by(user_id: user_2.id, email: 'confirmed-2@example.com').confirmed_at).to eq(user_2.reload.confirmed_at)
expect(emails.find_by(user_id: user_3.id, email: 'confirmed-3@example.com').confirmed_at).to eq(user_3.reload.confirmed_at)
expect(email_1.reload.confirmed_at).to eq(original_email_1_confirmed_at)
expect(emails.exists?(user_id: user_4.id)).to be(false)
end
it 'continues in case of errors with one email' do
allow(Email).to receive(:create) { raise 'boom!' }
expect { migration.up }.not_to raise_error
end
end

View file

@ -22,6 +22,7 @@ RSpec.shared_context 'project navbar structure' do
nav_sub_items: [
_('Activity'),
_('Labels'),
_('Planning hierarchy'),
_('Members')
]
},

View file

@ -17,7 +17,7 @@ RSpec.shared_context 'ProjectPolicy context' do
%i[
award_emoji create_issue create_merge_request_in create_note
create_project read_issue_board read_issue read_issue_iid read_issue_link
read_label read_issue_board_list read_milestone read_note read_project
read_label read_work_items_hierarchy read_issue_board_list read_milestone read_note read_project
read_project_for_iids read_project_member read_release read_snippet
read_wiki upload_file
]

View file

@ -48,7 +48,7 @@ class FindChanges # rubocop:disable Gitlab/NamespacedClass
mr_changes = Gitlab.merge_request_changes(mr_project_path, mr_iid)
mr_changes.changes.map { |change| change['new_path'] }
mr_changes.changes.map { |change| change['new_path'] unless change['deleted_file'] }.compact
end
end