summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-24 18:13:02 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-24 18:13:02 +0000
commitd48b87d4675d6b8b56dd9b40afa9eb2dce32ad3b (patch)
tree768c3d0900d3ba2910adf6abb24f433b8585be6c
parentfd9a56d56f84b36779fc4db2da37204c22585fe4 (diff)
downloadgitlab-ce-d48b87d4675d6b8b56dd9b40afa9eb2dce32ad3b.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js2
-rw-r--r--app/assets/javascripts/editor/constants.js1
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_extension_base.js45
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_trigger.vue25
-rw-r--r--app/assets/javascripts/invite_members/constants.js3
-rw-r--r--app/assets/javascripts/nav/components/top_nav_new_dropdown.vue17
-rw-r--r--app/assets/javascripts/pages/shared/wikis/wikis.js11
-rw-r--r--app/assets/stylesheets/page_bundles/wiki.scss44
-rw-r--r--app/helpers/nav/new_dropdown_helper.rb23
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/services/resource_access_tokens/create_service.rb16
-rw-r--r--app/views/groups/_invite_members_top_nav_link.html.haml9
-rw-r--r--app/views/layouts/header/_new_dropdown.html.haml2
-rw-r--r--app/views/projects/_invite_members_top_nav_link.html.haml9
-rw-r--r--app/views/projects/blob/_editor.html.haml16
-rw-r--r--app/views/shared/wikis/_wiki_directory.html.haml7
-rw-r--r--config/feature_flags/development/search_index_partitioning_notes.yml (renamed from config/feature_flags/development/delayed_repository_update_mirror_worker.yml)10
-rw-r--r--config/feature_flags/development/user_search_simple_query_string.yml8
-rw-r--r--db/post_migrate/20230223065753_finalize_nullify_creator_id_of_orphaned_projects.rb22
-rw-r--r--db/post_migrate/20230223093704_add_foreign_key_on_creator_id_on_projects.rb15
-rw-r--r--db/schema_migrations/202302230657531
-rw-r--r--db/schema_migrations/202302230937041
-rw-r--r--db/structure.sql3
-rw-r--r--doc/api/graphql/getting_started.md6
-rw-r--r--doc/api/project_import_export.md1
-rw-r--r--doc/development/database/batched_background_migrations.md93
-rw-r--r--doc/development/internal_api/index.md34
-rw-r--r--doc/user/group/settings/group_access_tokens.md12
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/user/project/issue_board.md2
-rw-r--r--doc/user/project/repository/forking_workflow.md8
-rw-r--r--doc/user/project/settings/project_access_tokens.md12
-rw-r--r--lib/api/issues.rb2
-rw-r--r--lib/gitlab/github_import/importer/label_links_importer.rb2
-rw-r--r--lib/gitlab/nav/top_nav_menu_item.rb5
-rw-r--r--locale/gitlab.pot2
-rw-r--r--package.json2
-rw-r--r--spec/db/schema_spec.rb2
-rw-r--r--spec/fast_spec_helper.rb2
-rw-r--r--spec/features/nav/top_nav_responsive_spec.rb15
-rw-r--r--spec/features/nav/top_nav_spec.rb14
-rw-r--r--spec/frontend/blob_edit/edit_blob_spec.js2
-rw-r--r--spec/frontend/editor/source_editor_extension_base_spec.js112
-rw-r--r--spec/frontend/invite_members/components/invite_members_trigger_spec.js36
-rw-r--r--spec/frontend/nav/components/top_nav_new_dropdown_spec.js25
-rw-r--r--spec/helpers/nav/new_dropdown_helper_spec.rb7
-rw-r--r--spec/helpers/sidebars_helper_spec.rb3
-rw-r--r--spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb3
-rw-r--r--spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb18
-rw-r--r--spec/lib/gitlab/nav/top_nav_menu_item_spec.rb3
-rw-r--r--spec/migrations/20230223065753_finalize_nullify_creator_id_of_orphaned_projects_spec.rb93
-rw-r--r--spec/models/issue_spec.rb5
-rw-r--r--spec/requests/api/issues/issues_spec.rb9
-rw-r--r--spec/services/resource_access_tokens/create_service_spec.rb18
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb24
-rw-r--r--spec/views/layouts/header/_new_dropdown.haml_spec.rb5
-rw-r--r--yarn.lock8
57 files changed, 753 insertions, 125 deletions
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index a3d11d90ed2..db47b11cca1 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -68,9 +68,9 @@ export default class EditBlob {
blobContent: editorEl.innerText,
});
this.editor.use([
+ { definition: ToolbarExtension },
{ definition: SourceEditorExtension },
{ definition: FileTemplateExtension },
- { definition: ToolbarExtension },
]);
fileNameEl.addEventListener('change', () => {
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
index 64b6c809870..2a47eef148e 100644
--- a/app/assets/javascripts/editor/constants.js
+++ b/app/assets/javascripts/editor/constants.js
@@ -166,3 +166,4 @@ export const EXTENSION_MARKDOWN_BUTTONS = [
},
},
];
+export const EXTENSION_SOFTWRAP_ID = 'soft-wrap';
diff --git a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
index 0590bb7455a..622e7117c07 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
@@ -1,8 +1,11 @@
import { Range } from 'monaco-editor';
+import { __ } from '~/locale';
import {
EDITOR_TYPE_CODE,
EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS,
EXTENSION_BASE_LINE_NUMBERS_CLASS,
+ EDITOR_TOOLBAR_RIGHT_GROUP,
+ EXTENSION_SOFTWRAP_ID,
} from '../constants';
const hashRegexp = /#?L/g;
@@ -24,6 +27,13 @@ export class SourceEditorExtension {
return 'BaseExtension';
}
+ onSetup(instance) {
+ this.toolbarButtons = [];
+ if (instance.toolbar) {
+ this.setupToolbar(instance);
+ }
+ }
+
// eslint-disable-next-line class-methods-use-this
onUse(instance) {
SourceEditorExtension.highlightLines(instance);
@@ -32,6 +42,31 @@ export class SourceEditorExtension {
}
}
+ onBeforeUnuse(instance) {
+ const ids = this.toolbarButtons.map((item) => item.id);
+ if (instance.toolbar) {
+ instance.toolbar.removeItems(ids);
+ }
+ }
+
+ setupToolbar(instance) {
+ this.toolbarButtons = [
+ {
+ id: EXTENSION_SOFTWRAP_ID,
+ label: __('Soft wrap'),
+ icon: 'soft-wrap',
+ selected: instance.getOption(116) === 'on',
+ group: EDITOR_TOOLBAR_RIGHT_GROUP,
+ category: 'primary',
+ selectedLabel: __('No wrap'),
+ selectedIcon: 'soft-unwrap',
+ class: 'soft-wrap-toggle',
+ onClick: () => instance.toggleSoftwrap(),
+ },
+ ];
+ instance.toolbar.addItems(this.toolbarButtons);
+ }
+
static onMouseMoveHandler(e) {
const target = e.target.element;
if (target.classList.contains(EXTENSION_BASE_LINE_NUMBERS_CLASS)) {
@@ -108,6 +143,16 @@ export class SourceEditorExtension {
highlightLines(instance, bounds = null) {
SourceEditorExtension.highlightLines(instance, bounds);
},
+
+ toggleSoftwrap(instance) {
+ const isSoftWrapped = instance.getOption(116) === 'on';
+ instance.updateOptions({ wordWrap: isSoftWrapped ? 'off' : 'on' });
+ if (instance.toolbar) {
+ instance.toolbar.updateItem(EXTENSION_SOFTWRAP_ID, {
+ selected: !isSoftWrapped,
+ });
+ }
+ },
};
}
}
diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
index 42645110e48..91c029081d4 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
@@ -1,15 +1,17 @@
<script>
-import { GlButton, GlLink, GlIcon } from '@gitlab/ui';
+import { GlButton, GlLink, GlIcon, GlDropdownItem } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
import {
TRIGGER_ELEMENT_BUTTON,
TRIGGER_ELEMENT_SIDE_NAV,
TRIGGER_DEFAULT_QA_SELECTOR,
+ TRIGGER_ELEMENT_WITH_EMOJI,
+ TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI,
} from '../constants';
export default {
- components: { GlButton, GlLink, GlIcon },
+ components: { GlButton, GlLink, GlIcon, GlDropdownItem },
props: {
displayText: {
type: String,
@@ -85,6 +87,8 @@ export default {
},
TRIGGER_ELEMENT_BUTTON,
TRIGGER_ELEMENT_SIDE_NAV,
+ TRIGGER_ELEMENT_WITH_EMOJI,
+ TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI,
};
</script>
@@ -109,6 +113,23 @@ export default {
</span>
<span class="nav-item-name"> {{ displayText }} </span>
</gl-link>
+ <gl-link
+ v-else-if="checkTrigger($options.TRIGGER_ELEMENT_WITH_EMOJI)"
+ v-bind="componentAttributes"
+ @click="openModal"
+ >
+ {{ displayText }}
+ <gl-emoji class="gl-vertical-align-baseline gl-reset-font-size gl-mr-1" :data-name="icon" />
+ </gl-link>
+ <gl-dropdown-item
+ v-else-if="checkTrigger($options.TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI)"
+ v-bind="componentAttributes"
+ button-class="top-nav-menu-item"
+ @click="openModal"
+ >
+ {{ displayText }}
+ <gl-emoji class="gl-vertical-align-baseline gl-reset-font-size gl-mr-1" :data-name="icon" />
+ </gl-dropdown-item>
<gl-link v-else v-bind="componentAttributes" data-is-link="true" @click="openModal">
{{ displayText }}
</gl-link>
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index ac0b708c55e..0e0734159bf 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -20,6 +20,9 @@ export const USERS_FILTER_ALL = 'all';
export const USERS_FILTER_SAML_PROVIDER_ID = 'saml_provider_id';
export const TRIGGER_ELEMENT_BUTTON = 'button';
export const TRIGGER_ELEMENT_SIDE_NAV = 'side-nav';
+export const TOP_NAV_INVITE_MEMBERS_COMPONENT = 'invite_members';
+export const TRIGGER_ELEMENT_WITH_EMOJI = 'text-emoji';
+export const TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI = 'dropdown-text-emoji';
export const INVITE_MEMBER_MODAL_TRACKING_CATEGORY = 'invite_members_modal';
export const TRIGGER_DEFAULT_QA_SELECTOR = 'invite_members_button';
export const MEMBERS_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite members');
diff --git a/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue b/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue
index bfcdcfc7292..2dfd77bc02e 100644
--- a/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue
+++ b/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue
@@ -1,5 +1,7 @@
<script>
import { GlDropdown, GlDropdownDivider, GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
+import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
+import { TOP_NAV_INVITE_MEMBERS_COMPONENT } from '~/invite_members/constants';
export default {
components: {
@@ -7,6 +9,7 @@ export default {
GlDropdownDivider,
GlDropdownItem,
GlDropdownSectionHeader,
+ InviteMembersTrigger,
},
props: {
viewModel: {
@@ -22,6 +25,11 @@ export default {
return this.sections.length > 1;
},
},
+ methods: {
+ isInvitedMembers(menuItem) {
+ return menuItem.component === TOP_NAV_INVITE_MEMBERS_COMPONENT;
+ },
+ },
};
</script>
@@ -41,7 +49,16 @@ export default {
{{ title }}
</gl-dropdown-section-header>
<template v-for="menuItem in menu_items">
+ <invite-members-trigger
+ v-if="isInvitedMembers(menuItem)"
+ :key="`${index}_item_${menuItem.id}`"
+ :trigger-element="`dropdown-${menuItem.data.trigger_element}`"
+ :display-text="menuItem.title"
+ :icon="menuItem.icon"
+ :trigger-source="menuItem.data.trigger_source"
+ />
<gl-dropdown-item
+ v-else
:key="`${index}_item_${menuItem.id}`"
link-class="top-nav-menu-item"
:href="menuItem.href"
diff --git a/app/assets/javascripts/pages/shared/wikis/wikis.js b/app/assets/javascripts/pages/shared/wikis/wikis.js
index 8d0105bc681..ec085eae199 100644
--- a/app/assets/javascripts/pages/shared/wikis/wikis.js
+++ b/app/assets/javascripts/pages/shared/wikis/wikis.js
@@ -16,6 +16,17 @@ export default class Wikis {
sidebarToggles[i].addEventListener('click', (e) => this.handleToggleSidebar(e));
}
+ const listToggles = document.querySelectorAll('.js-wiki-list-toggle');
+
+ listToggles.forEach((listToggle) => {
+ listToggle.querySelector('.js-wiki-list-expand-button')?.addEventListener('click', () => {
+ listToggle.classList.remove('collapsed');
+ });
+ listToggle.querySelector('.js-wiki-list-collapse-button')?.addEventListener('click', () => {
+ listToggle.classList.add('collapsed');
+ });
+ });
+
window.addEventListener('resize', () => this.renderSidebar());
this.renderSidebar();
diff --git a/app/assets/stylesheets/page_bundles/wiki.scss b/app/assets/stylesheets/page_bundles/wiki.scss
index 9bbea48d2c0..55628603570 100644
--- a/app/assets/stylesheets/page_bundles/wiki.scss
+++ b/app/assets/stylesheets/page_bundles/wiki.scss
@@ -106,6 +106,23 @@
color: var(--black, $black);
}
+ .active > .wiki-list {
+ a,
+ .wiki-list-expand-button,
+ .wiki-list-collapse-button {
+ color: var(--black, $black);
+ }
+ }
+
+ .wiki-list-expand-button,
+ .wiki-list-collapse-button {
+ color: var(--gray-400, $gray-400);
+
+ &:hover {
+ color: var(--black, $black);
+ }
+ }
+
ul.wiki-pages,
ul.wiki-pages li {
list-style: none;
@@ -118,7 +135,7 @@
}
ul.wiki-pages ul {
- padding-left: 15px;
+ padding-left: 20px;
}
.wiki-sidebar-header {
@@ -153,3 +170,28 @@ ul.wiki-pages-list.content-list {
.wiki-form .markdown-area {
max-height: 55vh;
}
+
+.wiki-list {
+ .wiki-list-expand-button,
+ .wiki-list-collapse-button {
+ left: -$gl-spacing-scale-5;
+ }
+
+ .wiki-list-expand-button {
+ display: none;
+ }
+
+ &.collapsed {
+ .wiki-list-collapse-button {
+ display: none;
+ }
+
+ .wiki-list-expand-button {
+ display: block;
+ }
+ }
+
+ &.collapsed + ul {
+ display: none;
+ }
+}
diff --git a/app/helpers/nav/new_dropdown_helper.rb b/app/helpers/nav/new_dropdown_helper.rb
index 98ff48bf19b..89211ed6a3e 100644
--- a/app/helpers/nav/new_dropdown_helper.rb
+++ b/app/helpers/nav/new_dropdown_helper.rb
@@ -53,12 +53,7 @@ module Nav
menu_items.push(create_epic_menu_item(group))
if can?(current_user, :admin_group_member, group)
- menu_items.push(
- invite_members_menu_item(
- href: group_group_members_path(group),
- partial: 'groups/invite_members_top_nav_link'
- )
- )
+ menu_items.push(invite_members_menu_item(partial: 'groups/invite_members_top_nav_link'))
end
{
@@ -105,12 +100,7 @@ module Nav
end
if can_admin_project_member?(project)
- menu_items.push(
- invite_members_menu_item(
- href: project_project_members_path(project),
- partial: 'projects/invite_members_top_nav_link'
- )
- )
+ menu_items.push(invite_members_menu_item(partial: 'projects/invite_members_top_nav_link'))
end
{
@@ -161,17 +151,16 @@ module Nav
}
end
- def invite_members_menu_item(href:, partial:)
+ def invite_members_menu_item(partial:)
::Gitlab::Nav::TopNavMenuItem.build(
id: 'invite',
title: s_('InviteMember|Invite members'),
icon: 'shaking_hands',
partial: partial,
- href: href,
+ component: 'invite_members',
data: {
- track_action: 'click_link_invite_members',
- track_label: 'plus_menu_dropdown',
- track_property: 'navigation_top'
+ trigger_source: 'top-nav',
+ trigger_element: 'text-emoji'
}
)
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 483bfca259c..adebb91f905 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -108,6 +108,7 @@ class Issue < ApplicationRecord
validates :issue_type, presence: true
validates :namespace, presence: true
validates :work_item_type, presence: true
+ validates :confidential, inclusion: { in: [true, false], message: 'must be a boolean' }
validate :allowed_work_item_type_change, on: :update, if: :work_item_type_id_changed?
validate :due_date_after_start_date
diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb
index f122fea6967..86fb2cdf114 100644
--- a/app/services/resource_access_tokens/create_service.rb
+++ b/app/services/resource_access_tokens/create_service.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'securerandom'
+
module ResourceAccessTokens
class CreateService < BaseService
def initialize(current_user, resource, params = {})
@@ -71,21 +73,15 @@ module ResourceAccessTokens
end
def generate_username
- base_username = "#{resource_type}_#{resource.id}_bot"
-
- uniquify.string(base_username) { |s| User.find_by_username(s) }
+ username
end
def generate_email
- email_pattern = "#{resource_type}#{resource.id}_bot%s@noreply.#{Gitlab.config.gitlab.host}"
-
- uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
- User.find_by_email(s)
- end
+ "#{username}@noreply.#{Gitlab.config.gitlab.host}"
end
- def uniquify
- Gitlab::Utils::Uniquify.new
+ def username
+ @username ||= "#{resource_type}_#{resource.id}_bot_#{SecureRandom.hex(8)}"
end
def create_personal_access_token(user)
diff --git a/app/views/groups/_invite_members_top_nav_link.html.haml b/app/views/groups/_invite_members_top_nav_link.html.haml
index a3a28ffd6e0..e419c479bca 100644
--- a/app/views/groups/_invite_members_top_nav_link.html.haml
+++ b/app/views/groups/_invite_members_top_nav_link.html.haml
@@ -1,6 +1,7 @@
-= link_to local_assigns.fetch(:href), class: local_assigns.fetch(:css_class), data: local_assigns.fetch(:data) do
- = local_assigns.fetch(:display_text)
- - if local_assigns.fetch(:icon)
- = " #{emoji_icon(local_assigns.fetch(:icon), 'aria-hidden': true, class: 'gl-font-base gl-vertical-align-baseline')}".html_safe
+- data = local_assigns.fetch(:data)
+- data[:display_text] = local_assigns.fetch(:display_text)
+- data[:icon] = local_assigns.fetch(:icon)
+
+.js-invite-members-trigger{ data: data }
= render 'groups/invite_members_modal', group: local_assigns.fetch(:context)
diff --git a/app/views/layouts/header/_new_dropdown.html.haml b/app/views/layouts/header/_new_dropdown.html.haml
index c133ce21c6c..42c2fd645da 100644
--- a/app/views/layouts/header/_new_dropdown.html.haml
+++ b/app/views/layouts/header/_new_dropdown.html.haml
@@ -31,8 +31,6 @@
locals: { context: view_model[:context],
display_text: menu_item.fetch(:title),
icon: menu_item.fetch(:icon),
- css_class: menu_item.fetch(:css_class),
- href: menu_item.fetch(:href),
data: menu_item.fetch(:data) }
- else
= link_to menu_item.fetch(:title),
diff --git a/app/views/projects/_invite_members_top_nav_link.html.haml b/app/views/projects/_invite_members_top_nav_link.html.haml
index e3a5c40d93c..d2e68325a09 100644
--- a/app/views/projects/_invite_members_top_nav_link.html.haml
+++ b/app/views/projects/_invite_members_top_nav_link.html.haml
@@ -1,6 +1,7 @@
-= link_to local_assigns.fetch(:href), class: local_assigns.fetch(:css_class), data: local_assigns.fetch(:data) do
- = local_assigns.fetch(:display_text)
- - if local_assigns.fetch(:icon)
- = " #{emoji_icon(local_assigns.fetch(:icon), 'aria-hidden': true, class: 'gl-font-base gl-vertical-align-baseline')}".html_safe
+- data = local_assigns.fetch(:data)
+- data[:display_text] = local_assigns.fetch(:display_text)
+- data[:icon] = local_assigns.fetch(:icon)
+
+.js-invite-members-trigger{ data: data }
= render 'projects/invite_members_modal', project: local_assigns.fetch(:context)
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 87a6b54d697..367413eb3b5 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -26,15 +26,15 @@
dismiss_key: @project.id,
human_access: human_access } }
- .file-buttons.gl-display-flex.gl-align-items-center.gl-justify-content-end
- - if is_markdown
- - unless Feature.enabled?(:source_editor_toolbar, current_user)
+ - unless Feature.enabled?(:source_editor_toolbar, current_user)
+ .file-buttons.gl-display-flex.gl-align-items-center.gl-justify-content-end
+ - if is_markdown
= render 'shared/blob/markdown_buttons', show_fullscreen_button: false, supports_file_upload: false
- %span.soft-wrap-toggle
- = render Pajamas::ButtonComponent.new(icon: 'soft-unwrap', button_options: { class: 'no-wrap' }) do
- = _("No wrap")
- = render Pajamas::ButtonComponent.new(icon: 'soft-wrap', button_options: { class: 'soft-wrap' }) do
- = _("Soft wrap")
+ %span.soft-wrap-toggle
+ = render Pajamas::ButtonComponent.new(icon: 'soft-unwrap', button_options: { class: 'no-wrap' }) do
+ = _("No wrap")
+ = render Pajamas::ButtonComponent.new(icon: 'soft-wrap', button_options: { class: 'soft-wrap' }) do
+ = _("Soft wrap")
.file-editor.code
- if Feature.enabled?(:source_editor_toolbar, current_user)
diff --git a/app/views/shared/wikis/_wiki_directory.html.haml b/app/views/shared/wikis/_wiki_directory.html.haml
index 5c2233a4db2..ced51e1f697 100644
--- a/app/views/shared/wikis/_wiki_directory.html.haml
+++ b/app/views/shared/wikis/_wiki_directory.html.haml
@@ -1,6 +1,9 @@
%li{ class: active_when(params[:id] == wiki_directory.slug), data: { qa_selector: 'wiki_directory_content' } }
- = link_to wiki_page_path(@wiki, wiki_directory), data: { qa_selector: 'wiki_dir_page_link', qa_page_name: wiki_directory.title } do
- = wiki_directory.title
+ .gl-relative.gl-display-flex.gl-align-items-center.js-wiki-list-toggle.wiki-list<
+ = sprite_icon('chevron-right', css_class: 'js-wiki-list-expand-button wiki-list-expand-button gl-mr-2 gl-cursor-pointer')
+ = sprite_icon('chevron-down', css_class: 'js-wiki-list-collapse-button wiki-list-collapse-button gl-mr-2 gl-cursor-pointer')
+ = link_to wiki_page_path(@wiki, wiki_directory), data: { qa_selector: 'wiki_dir_page_link', qa_page_name: wiki_directory.title } do
+ = wiki_directory.title
%ul
- wiki_directory.entries.each do |entry|
= render partial: entry.to_partial_path, object: entry, locals: { context: context }
diff --git a/config/feature_flags/development/delayed_repository_update_mirror_worker.yml b/config/feature_flags/development/search_index_partitioning_notes.yml
index acf5902716e..1abd3be35c5 100644
--- a/config/feature_flags/development/delayed_repository_update_mirror_worker.yml
+++ b/config/feature_flags/development/search_index_partitioning_notes.yml
@@ -1,8 +1,8 @@
---
-name: delayed_repository_update_mirror_worker
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89501
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/362894
-milestone: '15.1'
+name: search_index_partitioning_notes
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112402
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/392376
+milestone: '15.10'
type: development
-group: group::source code
+group: group::global search
default_enabled: false
diff --git a/config/feature_flags/development/user_search_simple_query_string.yml b/config/feature_flags/development/user_search_simple_query_string.yml
new file mode 100644
index 00000000000..78230c0fef9
--- /dev/null
+++ b/config/feature_flags/development/user_search_simple_query_string.yml
@@ -0,0 +1,8 @@
+---
+name: user_search_simple_query_string
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110623
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/391955
+milestone: '15.10'
+type: development
+group: group::global search
+default_enabled: false
diff --git a/db/post_migrate/20230223065753_finalize_nullify_creator_id_of_orphaned_projects.rb b/db/post_migrate/20230223065753_finalize_nullify_creator_id_of_orphaned_projects.rb
new file mode 100644
index 00000000000..aa3ed4837e7
--- /dev/null
+++ b/db/post_migrate/20230223065753_finalize_nullify_creator_id_of_orphaned_projects.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class FinalizeNullifyCreatorIdOfOrphanedProjects < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ MIGRATION = 'NullifyCreatorIdColumnOfOrphanedProjects'
+
+ def up
+ ensure_batched_background_migration_is_finished(
+ job_class_name: MIGRATION,
+ table_name: :projects,
+ column_name: :id,
+ job_arguments: []
+ )
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/post_migrate/20230223093704_add_foreign_key_on_creator_id_on_projects.rb b/db/post_migrate/20230223093704_add_foreign_key_on_creator_id_on_projects.rb
new file mode 100644
index 00000000000..68fd6de3f23
--- /dev/null
+++ b/db/post_migrate/20230223093704_add_foreign_key_on_creator_id_on_projects.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddForeignKeyOnCreatorIdOnProjects < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :projects, :users, column: :creator_id, on_delete: :nullify, validate: false
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key_if_exists :projects, column: :creator_id
+ end
+ end
+end
diff --git a/db/schema_migrations/20230223065753 b/db/schema_migrations/20230223065753
new file mode 100644
index 00000000000..c1b7927515f
--- /dev/null
+++ b/db/schema_migrations/20230223065753
@@ -0,0 +1 @@
+789d72eef2573834bef2a2d20070000b580eba069c45f97fdec18a4d5af99605 \ No newline at end of file
diff --git a/db/schema_migrations/20230223093704 b/db/schema_migrations/20230223093704
new file mode 100644
index 00000000000..bd35f5c493e
--- /dev/null
+++ b/db/schema_migrations/20230223093704
@@ -0,0 +1 @@
+39a17836884a6c07ff3f9df6e7328473f1dc2ac2d407f615821d29958f9b1808 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 4f8e8f04f6e..d6a51b7a5f7 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -33945,6 +33945,9 @@ ALTER TABLE ONLY service_desk_settings
ALTER TABLE ONLY design_management_designs_versions
ADD CONSTRAINT fk_03c671965c FOREIGN KEY (design_id) REFERENCES design_management_designs(id) ON DELETE CASCADE;
+ALTER TABLE ONLY projects
+ ADD CONSTRAINT fk_03ec10b0d3 FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE SET NULL NOT VALID;
+
ALTER TABLE ONLY analytics_dashboards_pointers
ADD CONSTRAINT fk_05d96922bd FOREIGN KEY (target_project_id) REFERENCES projects(id) ON DELETE CASCADE;
diff --git a/doc/api/graphql/getting_started.md b/doc/api/graphql/getting_started.md
index 57d7880988b..70e12df21f0 100644
--- a/doc/api/graphql/getting_started.md
+++ b/doc/api/graphql/getting_started.md
@@ -16,9 +16,9 @@ the API itself.
The examples documented here can be run using:
-- The command line.
-- GraphiQL.
-- Rails console.
+- [Command line](#command-line).
+- [GraphiQL](#graphiql).
+- [Rails console](#rails-console).
### Command line
diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md
index 00e73d41b46..29ad647f18d 100644
--- a/doc/api/project_import_export.md
+++ b/doc/api/project_import_export.md
@@ -135,6 +135,7 @@ POST /projects/import
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ---------------------------------------- |
+<!-- markdownlint-disable-next-line gitlab.SentenceSpacing -->
| `namespace` | integer/string | no | The ID or path of the namespace to import the project to. Defaults to the current user's namespace.<br/><br/>Requires at least the Maintainer role on the destination group to import to. Using the Developer role for this purpose was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/387891) in GitLab 15.8 and will be removed in GitLab 16.0. |
| `name` | string | no | The name of the project to be imported. Defaults to the path of the project if not provided |
| `file` | string | yes | The file to be uploaded |
diff --git a/doc/development/database/batched_background_migrations.md b/doc/development/database/batched_background_migrations.md
index 64e34fcc42d..67dccd99a6c 100644
--- a/doc/development/database/batched_background_migrations.md
+++ b/doc/development/database/batched_background_migrations.md
@@ -747,6 +747,99 @@ You can view failures in two ways:
WHERE transition_logs.next_status = '2' AND migration.job_class_name = "CLASS_NAME";
```
+### Executing a particular batch on the database testing pipeline
+
+NOTE:
+Only [database maintainers](https://gitlab.com/groups/gitlab-org/maintainers/database/-/group_members?with_inherited_permissions=exclude) can view the database testing pipeline artifacts. Ask one for help if you need to use this method.
+
+Let's assume that a batched background migration failed on a particular batch on GitLab.com and you want to figure out which query failed and why. At the moment, we don't have a good way to retrieve query information (especially the query parameters) and rerunning the entire migration with more logging would be a long process.
+
+Fortunately you can leverage our [database migration pipeline](database_migration_pipeline.md) to rerun a particular batch with additional logging and/or fix to see if it solves the problem.
+
+<!-- vale gitlab.Substitutions = NO -->
+For an example see [Draft: Test PG::CardinalityViolation fix](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110910) but make sure to read the entire section.
+
+To do that, you need to:
+
+1. Find the batch `start_id` and `end_id`
+1. Create a regular migration
+1. Apply a workaround for our migration helpers (optional)
+1. Start the database migration pipeline
+
+#### 1. Find the batch `start_id` and `end_id`
+
+You should be able to find those in [Kibana][#viewing-failure-error-logs].
+
+#### 2. Create a regular migration
+
+Schedule the batch in the `up` block of a regular migration:
+
+```ruby
+def up
+ instance = Gitlab::BackgroundMigration::YourBackgroundMigrationClass.new(
+ start_id: <batch start_id>,
+ end_id: <batch end_id>,
+ batch_table: <table name>,
+ batch_column: <batching column>,
+ sub_batch_size: <sub batch size>,
+ pause_ms: <miliseconds between batches>,
+ job_arguments: <job arguments if any>,
+ connection: connection
+ )
+
+ instance.perform
+end
+
+
+def down
+ # no-op
+end
+```
+
+#### 3. Apply a workaround for our migration helpers (optional)
+
+If your batched background migration touches tables from a schema other than the one you specified by using `restrict_gitlab_migration` helper (example: the scheduling migration has `restrict_gitlab_migration gitlab_schema: :gitlab_main` but the background job uses tables from the `:gitlab_ci` schema) then the migration will fail. To prevent that from happening you'll have to monkey patch database helpers so they don't fail the testing pipeline job:
+
+1. Add the schema names to [`RestrictGitlabSchema`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb#L57)
+
+```diff
+diff --git a/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb b/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb
+index b8d1d21a0d2d2a23d9e8c8a0a17db98ed1ed40b7..912e20659a6919f771045178c66828563cb5a4a1 100644
+--- a/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb
++++ b/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb
+@@ -55,7 +55,7 @@ def unmatched_schemas
+ end
+
+ def allowed_schemas_for_connection
+- Gitlab::Database.gitlab_schemas_for_connection(connection)
++ Gitlab::Database.gitlab_schemas_for_connection(connection) << :gitlab_ci
+ end
+ end
+ end
+```
+
+1. Add the schema names to [`RestrictAllowedSchemas`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb#L82)
+
+```diff
+diff --git a/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb b/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb
+index 4ae3622479f0800c0553959e132143ec9051898e..d556ec7f55adae9d46a56665ce02de782cb09f2d 100644
+--- a/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb
++++ b/lib/gitlab/database/query_analyzers/restrict_allowed_schemas.rb
+@@ -79,7 +79,7 @@ def restrict_to_dml_only(parsed)
+ tables = self.dml_tables(parsed)
+ schemas = self.dml_schemas(tables)
+
+- if (schemas - self.allowed_gitlab_schemas).any?
++ if (schemas - (self.allowed_gitlab_schemas << :gitlab_ci)).any?
+ raise DMLAccessDeniedError, \
+ "Select/DML queries (SELECT/UPDATE/DELETE) do access '#{tables}' (#{schemas.to_a}) " \
+ "which is outside of list of allowed schemas: '#{self.allowed_gitlab_schemas}'. " \
+```
+
+#### 4. Start the database migration pipeline
+
+Create a Draft merge request with your changes and trigger the manual `db:gitlabcom-database-testing` job.
+
### Adding indexes to support batched background migrations
Sometimes it is necessary to add a new or temporary index to support a batched background migration.
diff --git a/doc/development/internal_api/index.md b/doc/development/internal_api/index.md
index b19e431ebc6..aa10bbeda9c 100644
--- a/doc/development/internal_api/index.md
+++ b/doc/development/internal_api/index.md
@@ -37,13 +37,11 @@ is stored in a file at the path configured in `config/gitlab.yml` by
default this is in the root of the rails app named
`.gitlab_shell_secret`
-To authenticate using that token, clients read the contents of that
-file, and include the token Base64 encoded in a `secret_token` parameter
-or in the `Gitlab-Shared-Secret` header.
+To authenticate using that token, clients:
-NOTE:
-The internal API used by GitLab Pages, and GitLab agent server (`kas`) uses JSON Web Token (JWT)
-authentication, which is different from GitLab Shell.
+1. Read the contents of that file.
+1. Use the file contents to generate a JSON Web Token (`JWT`).
+1. Pass the JWT in the `Gitlab-Shell-Api-Request` header.
## Git Authentication
@@ -78,7 +76,7 @@ POST /internal/allowed
Example request:
```shell
-curl --request POST --header "Gitlab-Shared-Secret: <Base64 encoded token>" \
+curl --request POST --header "Gitlab-Shell-Api-Request: <JWT token>" \
--data "key_id=11&project=gnuwget/wget2&action=git-upload-pack&protocol=ssh" \
"http://localhost:3001/api/v4/internal/allowed"
```
@@ -128,7 +126,7 @@ information for LFS clients when the repository is accessed over SSH.
Example request:
```shell
-curl --request POST --header "Gitlab-Shared-Secret: <Base64 encoded token>" \
+curl --request POST --header "Gitlab-Shell-Api-Request: <JWT token>" \
--data "key_id=11&project=gnuwget/wget2" "http://localhost:3001/api/v4/internal/lfs_authenticate"
```
@@ -148,12 +146,12 @@ curl --request POST --header "Gitlab-Shared-Secret: <Base64 encoded token>" \
## Authorized Keys Check
This endpoint is called by the GitLab Shell authorized keys
-check. Which is called by OpenSSH for
+check. Which is called by OpenSSH or GitLab SSHD for
[fast SSH key lookup](../../administration/operations/fast_ssh_key_lookup.md).
| Attribute | Type | Required | Description |
|:----------|:-------|:---------|:------------|
-| `key` | string | yes | SSH key as passed by OpenSSH to GitLab Shell |
+| `key` | string | yes | An authorized key used for public key authentication. |
```plaintext
GET /internal/authorized_keys
@@ -162,7 +160,7 @@ GET /internal/authorized_keys
Example request:
```shell
-curl --request GET --header "Gitlab-Shared-Secret: <Base64 encoded secret>" "http://localhost:3001/api/v4/internal/authorized_keys?key=<key as passed by OpenSSH>"
+curl --request GET --header "Gitlab-Shell-Api-Request: <JWT token>" "http://localhost:3001/api/v4/internal/authorized_keys?key=<key>"
```
Example response:
@@ -197,7 +195,7 @@ GET /internal/discover
Example request:
```shell
-curl --request GET --header "Gitlab-Shared-Secret: <Base64 encoded secret>" "http://localhost:3001/api/v4/internal/discover?key_id=7"
+curl --request GET --header "Gitlab-Shell-Api-Request: <JWT token>" "http://localhost:3001/api/v4/internal/discover?key_id=7"
```
Example response:
@@ -226,7 +224,7 @@ GET /internal/check
Example request:
```shell
-curl --request GET --header "Gitlab-Shared-Secret: <Base64 encoded secret>" "http://localhost:3001/api/v4/internal/check"
+curl --request GET --header "Gitlab-Shell-Api-Request: <JWT token>" "http://localhost:3001/api/v4/internal/check"
```
Example response:
@@ -263,7 +261,7 @@ GET /internal/two_factor_recovery_codes
Example request:
```shell
-curl --request POST --header "Gitlab-Shared-Secret: <Base64 encoded secret>" \
+curl --request POST --header "Gitlab-Shell-Api-Request: <JWT token>" \
--data "key_id=7" "http://localhost:3001/api/v4/internal/two_factor_recovery_codes"
```
@@ -311,7 +309,7 @@ POST /internal/personal_access_token
Example request:
```shell
-curl --request POST --header "Gitlab-Shared-Secret: <Base64 encoded secret>" \
+curl --request POST --header "Gitlab-Shell-Api-Request: <JWT token>" \
--data "user_id=29&name=mytokenname&scopes[]=read_user&scopes[]=read_repository&expires_at=2020-07-24" \
"http://localhost:3001/api/v4/internal/personal_access_token"
```
@@ -348,7 +346,7 @@ POST /internal/error_tracking/allowed
Example request:
```shell
-curl --request POST --header "Gitlab-Shared-Secret: <Base64 encoded secret>" \
+curl --request POST --header "Gitlab-Shell-Api-Request: <JWT token>" \
--data "project_id=111&public_key=generated-error-tracking-key" \
"http://localhost:3001/api/v4/internal/error_tracking/allowed"
```
@@ -379,7 +377,7 @@ POST /internal/pre_receive
Example request:
```shell
-curl --request POST --header "Gitlab-Shared-Secret: <Base64 encoded secret>" \
+curl --request POST --header "Gitlab-Shell-Api-Request: <JWT token>" \
--data "gl_repository=project-7" "http://localhost:3001/api/v4/internal/pre_receive"
```
@@ -412,7 +410,7 @@ POST /internal/post_receive
Example Request:
```shell
-curl --request POST --header "Gitlab-Shared-Secret: <Base64 encoded secret>" \
+curl --request POST --header "Gitlab-Shell-Api-Request: <JWT token>" \
--data "gl_repository=project-7" --data "identifier=user-1" \
--data "changes=0000000000000000000000000000000000000000 fd9e76b9136bdd9fe217061b497745792fe5a5ee gh-pages\n" \
"http://localhost:3001/api/v4/internal/post_receive"
diff --git a/doc/user/group/settings/group_access_tokens.md b/doc/user/group/settings/group_access_tokens.md
index dc499b96f3c..29170af74f9 100644
--- a/doc/user/group/settings/group_access_tokens.md
+++ b/doc/user/group/settings/group_access_tokens.md
@@ -84,8 +84,8 @@ or API. However, administrators can use a workaround:
# Set the group group you want to create a token for. For example, group with ID 109.
group = Group.find(109)
- # Create the group bot user. For further group access tokens, the username should be group_#{group.id}_bot#{bot_count}. For example, group_109_bot2 and email address group_109_bot2@example.com.
- bot = Users::CreateService.new(admin, { name: 'group_token', username: "group_#{group.id}_bot", email: "group_#{group.id}_bot@example.com", user_type: :project_bot }).execute
+ # Create the group bot user. For further group access tokens, the username should be group_#{group.id}_bot#{bot_count}. For example, group_109_bot2 and email address group_109_bot_{random_string}@example.com.
+ bot = Users::CreateService.new(admin, { name: 'group_token', username: "group_#{group.id}_bot", email: "group_#{group.id}_bot_4ffca233d8298ea1@example.com", user_type: :project_bot }).execute
# Confirm the group bot.
bot.confirm
@@ -169,7 +169,11 @@ to groups instead of projects. Bot users for groups:
- Do not count as licensed seats.
- Can have a maximum role of Owner for a group. For more information, see
[Create a group access token](../../../api/group_access_tokens.md#create-a-group-access-token).
-- Have a username set to `group_{group_id}_bot` for the first access token. For example, `group_123_bot`.
-- Have an email set to `group{group_id}_bot@noreply.{Gitlab.config.gitlab.host}`. For example, `group123_bot@noreply.example.com`.
+- Have a username set to `group_{group_id}_bot_{random_string}`. For example, `group_123_bot_4ffca233d8298ea1`.
+- Have an email set to `group{group_id}_bot_{random_string}@noreply.{Gitlab.config.gitlab.host}`. For example, `group123_bot_4ffca233d8298ea1@noreply.example.com`.
All other properties are similar to [bot users for projects](../../project/settings/project_access_tokens.md#bot-users-for-projects).
+
+## Token availability
+
+Group access tokens are only available in paid subscriptions, and not available in trial subscriptions. For more information, see the ["What is included" section of the GitLab Trial FAQ](https://about.gitlab.com/free-trial/#what-is-included-in-my-free-trial-what-is-excluded).
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index ca2fdeb98ae..aa292ca18a3 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -511,7 +511,7 @@ To remove a custom role from a group member, use the [Group and Project Members
and pass an empty `member_role_id` value.
```shell
-curl --request DELETE --header "Content-Type: application/json" --header "Authorization: Bearer $YOUR_ACCESS_TOKEN" --data '{"member_role_id": "", "access_level": 10}' "https://example.gitlab.com/api/v4/groups/$GROUP_PATH/members/$GUEST_USER_ID"
+curl --request PUT --header "Content-Type: application/json" --header "Authorization: Bearer $YOUR_ACCESS_TOKEN" --data '{"member_role_id": "", "access_level": 10}' "https://example.gitlab.com/api/v4/groups/$GROUP_PATH/members/$GUEST_USER_ID"
```
Now the user is a regular Guest.
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index 1862e8d01bc..98017fb5542 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -417,7 +417,7 @@ To set a WIP limit for a list, in an issue board:
1. On the top of the list you want to edit, select **List actions** (**{ellipsis_v}**) **> Edit list settings**.
The list settings sidebar opens on the right.
-1. Next to **Work in progress Limit**, select **Edit**.
+1. Next to **Work in progress limit**, select **Edit**.
1. Enter the maximum number of issues.
1. Press <kbd>Enter</kbd> to save.
diff --git a/doc/user/project/repository/forking_workflow.md b/doc/user/project/repository/forking_workflow.md
index ea45de4efc4..8c7c94613a7 100644
--- a/doc/user/project/repository/forking_workflow.md
+++ b/doc/user/project/repository/forking_workflow.md
@@ -137,3 +137,11 @@ You can unlink your fork from its upstream project in the [advanced settings](..
- GitLab blog post: [How to keep your fork up to date with its origin](https://about.gitlab.com/blog/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/).
- GitLab community forum: [Refreshing a fork](https://forum.gitlab.com/t/refreshing-a-fork/).
+
+## Troubleshooting
+
+### An error occurred while forking the project. Please try again
+
+This error can be due to a mismatch in shared runner settings between the forked project
+and the new namespace. See [Forks](../../../ci/runners/configure_runners.md#forks)
+in the Runner documentation for more information.
diff --git a/doc/user/project/settings/project_access_tokens.md b/doc/user/project/settings/project_access_tokens.md
index 19db5032ea9..4fba6e0d5b8 100644
--- a/doc/user/project/settings/project_access_tokens.md
+++ b/doc/user/project/settings/project_access_tokens.md
@@ -118,12 +118,8 @@ The bot users for projects have [permissions](../../permissions.md#project-membe
selected role and [scope](#scopes-for-a-project-access-token) of the project access token.
- The name is set to the name of the token.
-- The username is set to `project_{project_id}_bot` for the first access token. For example, `project_123_bot`.
-- The email is set to `project{project_id}_bot@noreply.{Gitlab.config.gitlab.host}`. For example, `project123_bot@noreply.example.com`.
-- For additional access tokens in the same project, the username is set to `project_{project_id}_bot{bot_count}`. For
- example, `project_123_bot1`.
-- For additional access tokens in the same project, the email is set to `project{project_id}_bot{bot_count}@noreply.{Gitlab.config.gitlab.host}`.
- For example, `project123_bot1@noreply.example.com`.
+- The username is set to `project_{project_id}_bot_{random_string}`. For example, `project_123_bot_4ffca233d8298ea1`.
+- The email is set to `project{project_id}_bot_{random_string}@noreply.{Gitlab.config.gitlab.host}`. For example, `project123_bot_4ffca233d8298ea1@noreply.example.com`.
API calls made with a project access token are associated with the corresponding bot user.
@@ -140,3 +136,7 @@ When the project access token is [revoked](#revoke-a-project-access-token):
- All records are moved to a system-wide user with the username [Ghost User](../../profile/account/delete_account.md#associated-records).
See also [Bot users for groups](../../group/settings/group_access_tokens.md#bot-users-for-groups).
+
+## Token availability
+
+Project access tokens are only available in paid subscriptions, and not available in trial subscriptions. For more information, see the ["What is included" section of the GitLab Trial FAQ](https://about.gitlab.com/free-trial/#what-is-included-in-my-free-trial-what-is-excluded).
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 88e99b29587..d033913aa71 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -99,7 +99,7 @@ module API
optional :add_labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
optional :remove_labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY'
- optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
+ optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential', allow_blank: false
optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked"
optional :issue_type, type: String, values: WorkItems::Type.allowed_types_for_issues, desc: "The type of the issue. Accepts: #{WorkItems::Type.allowed_types_for_issues.join(', ')}"
diff --git a/lib/gitlab/github_import/importer/label_links_importer.rb b/lib/gitlab/github_import/importer/label_links_importer.rb
index 52c87dda347..a20fec4b2ba 100644
--- a/lib/gitlab/github_import/importer/label_links_importer.rb
+++ b/lib/gitlab/github_import/importer/label_links_importer.rb
@@ -25,6 +25,8 @@ module Gitlab
items = []
target_id = find_target_id
+ return if target_id.blank?
+
issue.label_names.each do |label_name|
# Although unlikely it's technically possible for an issue to be
# given a label that was created and assigned after we imported all
diff --git a/lib/gitlab/nav/top_nav_menu_item.rb b/lib/gitlab/nav/top_nav_menu_item.rb
index af88f55014c..a83cdbe15df 100644
--- a/lib/gitlab/nav/top_nav_menu_item.rb
+++ b/lib/gitlab/nav/top_nav_menu_item.rb
@@ -10,7 +10,7 @@ module Gitlab
# rubocop: disable Metrics/ParameterLists
def self.build(
id:, title:, active: false, icon: '', href: '', view: '',
- css_class: nil, data: nil, partial: nil
+ css_class: nil, data: nil, partial: nil, component: nil
)
{
id: id,
@@ -22,7 +22,8 @@ module Gitlab
view: view.to_s,
css_class: css_class,
data: data || { qa_selector: 'menu_item_link', qa_title: title },
- partial: partial
+ partial: partial,
+ component: component
}
end
# rubocop: enable Metrics/ParameterLists
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8eabffd78b7..800abad7e33 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -48616,7 +48616,7 @@ msgstr ""
msgid "Work in progress (open and unassigned)"
msgstr ""
-msgid "Work in progress Limit"
+msgid "Work in progress limit"
msgstr ""
msgid "WorkItem|%{count} more assignees"
diff --git a/package.json b/package.json
index 7c4b4e2542b..e5081047ee8 100644
--- a/package.json
+++ b/package.json
@@ -58,7 +58,7 @@
"@gitlab/svgs": "3.21.0",
"@gitlab/ui": "56.1.2",
"@gitlab/visual-review-tools": "1.7.3",
- "@gitlab/web-ide": "0.0.1-dev-20230216131813",
+ "@gitlab/web-ide": "0.0.1-dev-20230223005157",
"@rails/actioncable": "6.1.4-7",
"@rails/ujs": "6.1.4-7",
"@sourcegraph/code-host-integration": "0.0.84",
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 6a7539aeb2f..756cb23b7a4 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -95,7 +95,7 @@ RSpec.describe 'Database schema', feature_category: :database do
project_error_tracking_settings: %w[sentry_project_id],
project_group_links: %w[group_id],
project_statistics: %w[namespace_id],
- projects: %w[creator_id ci_id mirror_user_id],
+ projects: %w[ci_id mirror_user_id],
redirect_routes: %w[source_id],
repository_languages: %w[programming_language_id],
routes: %w[source_id],
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
index e8d58656178..8395124cbe4 100644
--- a/spec/fast_spec_helper.rb
+++ b/spec/fast_spec_helper.rb
@@ -14,13 +14,13 @@ ENV['IN_MEMORY_APPLICATION_SETTINGS'] = 'true'
# Enable zero monkey patching mode before loading any other RSpec code.
RSpec.configure(&:disable_monkey_patching!)
+require 'active_support/all'
require_relative 'rails_autoload'
require_relative '../config/settings'
require_relative 'support/rspec'
require_relative '../lib/gitlab/utils'
require_relative '../lib/gitlab/utils/strong_memoize'
-require 'active_support/all'
require_relative 'simplecov_env'
SimpleCovEnv.start!
diff --git a/spec/features/nav/top_nav_responsive_spec.rb b/spec/features/nav/top_nav_responsive_spec.rb
index 56f9d373f00..f1862a3b9b2 100644
--- a/spec/features/nav/top_nav_responsive_spec.rb
+++ b/spec/features/nav/top_nav_responsive_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
include MobileHelpers
+ include Spec::Support::Helpers::Features::InviteMembersModalHelper
let_it_be(:user) { create(:user) }
@@ -61,10 +62,12 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
visit project_path(project)
end
- it 'the add menu contains invite members dropdown option and goes to the members page' do
+ it 'the add menu contains invite members dropdown option and opens invite modal' do
invite_members_from_menu
- expect(page).to have_current_path(project_project_members_path(project))
+ page.within invite_modal_selector do
+ expect(page).to have_content("You're inviting members to the #{project.name} project")
+ end
end
end
@@ -75,10 +78,12 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
visit group_path(group)
end
- it 'the add menu contains invite members dropdown option and goes to the members page' do
+ it 'the add menu contains invite members dropdown option and opens invite modal' do
invite_members_from_menu
- expect(page).to have_current_path(group_group_members_path(group))
+ page.within invite_modal_selector do
+ expect(page).to have_content("You're inviting members to the #{group.name} group")
+ end
end
end
@@ -86,7 +91,7 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
click_button('Menu')
create_new_button.click
- click_link('Invite members')
+ click_button('Invite members')
end
def create_new_button
diff --git a/spec/features/nav/top_nav_spec.rb b/spec/features/nav/top_nav_spec.rb
index cc20b626e30..d2c0286cb4d 100644
--- a/spec/features/nav/top_nav_spec.rb
+++ b/spec/features/nav/top_nav_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
+ include Spec::Support::Helpers::Features::InviteMembersModalHelper
+
let_it_be(:user) { create(:user) }
before do
@@ -16,10 +18,12 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
visit project_path(project)
end
- it 'the add menu contains invite members dropdown option and goes to the members page' do
+ it 'the add menu contains invite members dropdown option and opens invite modal' do
invite_members_from_menu
- expect(page).to have_current_path(project_project_members_path(project))
+ page.within invite_modal_selector do
+ expect(page).to have_content("You're inviting members to the #{project.name} project")
+ end
end
end
@@ -30,10 +34,12 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
visit group_path(group)
end
- it 'the add menu contains invite members dropdown option and goes to the members page' do
+ it 'the add menu contains invite members dropdown option and opens invite modal' do
invite_members_from_menu
- expect(page).to have_current_path(group_group_members_path(group))
+ page.within invite_modal_selector do
+ expect(page).to have_content("You're inviting members to the #{group.name} group")
+ end
end
end
diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js
index dda46e97b85..9ab20fc2cd7 100644
--- a/spec/frontend/blob_edit/edit_blob_spec.js
+++ b/spec/frontend/blob_edit/edit_blob_spec.js
@@ -20,9 +20,9 @@ jest.mock('~/editor/extensions/source_editor_toolbar_ext');
const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown';
const defaultExtensions = [
+ { definition: ToolbarExtension },
{ definition: SourceEditorExtension },
{ definition: FileTemplateExtension },
- { definition: ToolbarExtension },
];
const markdownExtensions = [
{ definition: EditorMarkdownExtension },
diff --git a/spec/frontend/editor/source_editor_extension_base_spec.js b/spec/frontend/editor/source_editor_extension_base_spec.js
index eab39ccaba1..b1b8173188c 100644
--- a/spec/frontend/editor/source_editor_extension_base_spec.js
+++ b/spec/frontend/editor/source_editor_extension_base_spec.js
@@ -7,6 +7,7 @@ import {
EDITOR_TYPE_DIFF,
EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS,
EXTENSION_BASE_LINE_NUMBERS_CLASS,
+ EXTENSION_SOFTWRAP_ID,
} from '~/editor/constants';
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import EditorInstance from '~/editor/source_editor_instance';
@@ -35,8 +36,18 @@ describe('The basis for an Source Editor extension', () => {
},
};
};
- const createInstance = (baseInstance = {}) => {
- return new EditorInstance(baseInstance);
+ const baseInstance = {
+ getOption: jest.fn(),
+ };
+
+ const createInstance = (base = baseInstance) => {
+ return new EditorInstance(base);
+ };
+
+ const toolbar = {
+ addItems: jest.fn(),
+ updateItem: jest.fn(),
+ removeItems: jest.fn(),
};
beforeEach(() => {
@@ -49,6 +60,66 @@ describe('The basis for an Source Editor extension', () => {
resetHTMLFixture();
});
+ describe('onSetup callback', () => {
+ let instance;
+ beforeEach(() => {
+ instance = createInstance();
+
+ instance.toolbar = toolbar;
+ });
+
+ it('adds correct buttons to the toolbar', () => {
+ instance.use({ definition: SourceEditorExtension });
+ expect(instance.toolbar.addItems).toHaveBeenCalledWith([
+ expect.objectContaining({
+ id: EXTENSION_SOFTWRAP_ID,
+ }),
+ ]);
+ });
+
+ it('does not fail if toolbar is not available', () => {
+ instance.toolbar = null;
+ expect(() => instance.use({ definition: SourceEditorExtension })).not.toThrow();
+ });
+
+ it.each`
+ optionValue | expectSelected
+ ${'on'} | ${true}
+ ${'off'} | ${false}
+ ${'foo'} | ${false}
+ ${undefined} | ${false}
+ ${null} | ${false}
+ `(
+ 'correctly sets the initial state of the button when wordWrap option is "$optionValue"',
+ ({ optionValue, expectSelected }) => {
+ instance.getOption.mockReturnValue(optionValue);
+ instance.use({ definition: SourceEditorExtension });
+ expect(instance.toolbar.addItems).toHaveBeenCalledWith([
+ expect.objectContaining({
+ selected: expectSelected,
+ }),
+ ]);
+ },
+ );
+ });
+
+ describe('onBeforeUnuse', () => {
+ let instance;
+ let extension;
+
+ beforeEach(() => {
+ instance = createInstance();
+
+ instance.toolbar = toolbar;
+ extension = instance.use({ definition: SourceEditorExtension });
+ });
+ it('removes the registered buttons from the toolbar', () => {
+ expect(instance.toolbar.removeItems).not.toHaveBeenCalled();
+ instance.unuse(extension);
+ expect(instance.toolbar.removeItems).toHaveBeenCalledWith([EXTENSION_SOFTWRAP_ID]);
+ });
+ });
+
describe('onUse callback', () => {
it('initializes the line highlighting', () => {
const instance = createInstance();
@@ -66,6 +137,7 @@ describe('The basis for an Source Editor extension', () => {
'$description the line linking for $instanceType instance',
({ instanceType, shouldBeCalled }) => {
const instance = createInstance({
+ ...baseInstance,
getEditorType: jest.fn().mockReturnValue(instanceType),
onMouseMove: jest.fn(),
onMouseDown: jest.fn(),
@@ -82,10 +154,44 @@ describe('The basis for an Source Editor extension', () => {
);
});
+ describe('toggleSoftwrap', () => {
+ let instance;
+
+ beforeEach(() => {
+ instance = createInstance();
+
+ instance.toolbar = toolbar;
+ instance.use({ definition: SourceEditorExtension });
+ });
+
+ it.each`
+ currentWordWrap | newWordWrap | expectSelected
+ ${'on'} | ${'off'} | ${false}
+ ${'off'} | ${'on'} | ${true}
+ ${'foo'} | ${'on'} | ${true}
+ ${undefined} | ${'on'} | ${true}
+ ${null} | ${'on'} | ${true}
+ `(
+ 'correctly updates wordWrap option in editor and the state of the button when currentWordWrap is "$currentWordWrap"',
+ ({ currentWordWrap, newWordWrap, expectSelected }) => {
+ instance.getOption.mockReturnValue(currentWordWrap);
+ instance.updateOptions = jest.fn();
+ instance.toggleSoftwrap();
+ expect(instance.updateOptions).toHaveBeenCalledWith({
+ wordWrap: newWordWrap,
+ });
+ expect(instance.toolbar.updateItem).toHaveBeenCalledWith(EXTENSION_SOFTWRAP_ID, {
+ selected: expectSelected,
+ });
+ },
+ );
+ });
+
describe('highlightLines', () => {
const revealSpy = jest.fn();
const decorationsSpy = jest.fn();
const instance = createInstance({
+ ...baseInstance,
revealLineInCenter: revealSpy,
deltaDecorations: decorationsSpy,
});
@@ -174,6 +280,7 @@ describe('The basis for an Source Editor extension', () => {
beforeEach(() => {
instance = createInstance({
+ ...baseInstance,
deltaDecorations: decorationsSpy,
lineDecorations,
});
@@ -188,6 +295,7 @@ describe('The basis for an Source Editor extension', () => {
describe('setupLineLinking', () => {
const instance = {
+ ...baseInstance,
onMouseMove: jest.fn(),
onMouseDown: jest.fn(),
deltaDecorations: jest.fn(),
diff --git a/spec/frontend/invite_members/components/invite_members_trigger_spec.js b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
index c522abe63c5..e5af38d6ed8 100644
--- a/spec/frontend/invite_members/components/invite_members_trigger_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlLink, GlIcon } from '@gitlab/ui';
+import { GlButton, GlLink, GlIcon, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import eventHub from '~/invite_members/event_hub';
@@ -6,7 +6,10 @@ import {
TRIGGER_ELEMENT_BUTTON,
TRIGGER_ELEMENT_SIDE_NAV,
TRIGGER_DEFAULT_QA_SELECTOR,
+ TRIGGER_ELEMENT_WITH_EMOJI,
+ TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI,
} from '~/invite_members/constants';
+import { GlEmoji } from '../mock_data/member_modal';
jest.mock('~/experimentation/experiment_tracking');
@@ -20,6 +23,8 @@ const triggerComponent = {
button: GlButton,
anchor: GlLink,
'side-nav': GlLink,
+ 'text-emoji': GlLink,
+ 'dropdown-text-emoji': GlDropdownItem,
};
const createComponent = (props = {}) => {
@@ -29,6 +34,9 @@ const createComponent = (props = {}) => {
...triggerProps,
...props,
},
+ stubs: {
+ GlEmoji,
+ },
});
};
@@ -43,6 +51,10 @@ const triggerItems = [
triggerElement: TRIGGER_ELEMENT_SIDE_NAV,
icon: 'plus',
},
+ {
+ triggerElement: TRIGGER_ELEMENT_WITH_EMOJI,
+ icon: 'shaking_hands',
+ },
];
describe.each(triggerItems)('with triggerElement as %s', (triggerItem) => {
@@ -119,3 +131,25 @@ describe('side-nav with icon', () => {
expect(findIcon().props('name')).toBe('plus');
});
});
+
+describe('link with emoji', () => {
+ it('includes the specified icon with correct size when triggerElement is link', () => {
+ const findEmoji = () => wrapper.findComponent(GlEmoji);
+
+ createComponent({ triggerElement: TRIGGER_ELEMENT_WITH_EMOJI, icon: 'shaking_hands' });
+
+ expect(findEmoji().exists()).toBe(true);
+ expect(findEmoji().attributes('data-name')).toBe('shaking_hands');
+ });
+});
+
+describe('dropdown item with emoji', () => {
+ it('includes the specified icon with correct size when triggerElement is link', () => {
+ const findEmoji = () => wrapper.findComponent(GlEmoji);
+
+ createComponent({ triggerElement: TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI, icon: 'shaking_hands' });
+
+ expect(findEmoji().exists()).toBe(true);
+ expect(findEmoji().attributes('data-name')).toBe('shaking_hands');
+ });
+});
diff --git a/spec/frontend/nav/components/top_nav_new_dropdown_spec.js b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js
index 18210658b89..533650cd149 100644
--- a/spec/frontend/nav/components/top_nav_new_dropdown_spec.js
+++ b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js
@@ -1,6 +1,8 @@
import { GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import TopNavNewDropdown from '~/nav/components/top_nav_new_dropdown.vue';
+import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
+import { TOP_NAV_INVITE_MEMBERS_COMPONENT } from '~/invite_members/constants';
const TEST_VIEW_MODEL = {
title: 'Dropdown',
@@ -18,6 +20,16 @@ const TEST_VIEW_MODEL = {
menu_items: [
{ id: 'bar-1', title: 'Bar 1', href: '/bar/1' },
{ id: 'bar-2', title: 'Bar 2', href: '/bar/2' },
+ {
+ id: 'invite',
+ title: '_invite members title_',
+ component: TOP_NAV_INVITE_MEMBERS_COMPONENT,
+ icon: '_icon_',
+ data: {
+ trigger_element: '_trigger_element_',
+ trigger_source: '_trigger_source_',
+ },
+ },
],
},
],
@@ -36,6 +48,7 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger);
const findDropdownContents = () =>
findDropdown()
.findAll('[data-testid]')
@@ -73,6 +86,10 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
});
it('renders dropdown content', () => {
+ const hrefItems = TEST_VIEW_MODEL.menu_sections[1].menu_items.filter((item) =>
+ Boolean(item.href),
+ );
+
expect(findDropdownContents()).toEqual([
{
type: 'header',
@@ -90,12 +107,18 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
type: 'header',
text: TEST_VIEW_MODEL.menu_sections[1].title,
},
- ...TEST_VIEW_MODEL.menu_sections[1].menu_items.map(({ title, href }) => ({
+ ...hrefItems.map(({ title, href }) => ({
type: 'item',
href,
text: title,
})),
]);
+ expect(findInviteMembersTrigger().props()).toMatchObject({
+ displayText: '_invite members title_',
+ icon: '_icon_',
+ triggerElement: 'dropdown-_trigger_element_',
+ triggerSource: '_trigger_source_',
+ });
});
});
diff --git a/spec/helpers/nav/new_dropdown_helper_spec.rb b/spec/helpers/nav/new_dropdown_helper_spec.rb
index 1df0ee8c18a..174a5a668a8 100644
--- a/spec/helpers/nav/new_dropdown_helper_spec.rb
+++ b/spec/helpers/nav/new_dropdown_helper_spec.rb
@@ -35,12 +35,11 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
id: 'invite',
title: 'Invite members',
icon: 'shaking_hands',
- href: expected_href,
partial: partial,
+ component: 'invite_members',
data: {
- track_action: 'click_link_invite_members',
- track_label: 'plus_menu_dropdown',
- track_property: 'navigation_top'
+ trigger_source: 'top-nav',
+ trigger_element: 'text-emoji'
}
)
)
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb
index fa4d4071e37..aa6b1dba6e8 100644
--- a/spec/helpers/sidebars_helper_spec.rb
+++ b/spec/helpers/sidebars_helper_spec.rb
@@ -170,8 +170,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
items: array_including(
{ href: "/projects/new", text: "New project/repository" },
{ href: "/groups/new#create-group-pane", text: "New subgroup" },
- { href: "/groups/#{group.full_path}/-/group_members",
- text: "Invite members" }
+ { href: '', text: "Invite members" }
)
),
a_hash_including(
diff --git a/spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb b/spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb
index a8574411957..f671a673a08 100644
--- a/spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb
+++ b/spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::NullifyCreatorIdColumnOfOrphanedProjects, feature_category: :projects do
+RSpec.describe Gitlab::BackgroundMigration::NullifyCreatorIdColumnOfOrphanedProjects, feature_category: :projects,
+ schema: 20230130073109 do
let(:users) { table(:users) }
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
diff --git a/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
index e005d8eda84..16816dfbcea 100644
--- a/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
@@ -44,6 +44,10 @@ RSpec.describe Gitlab::GithubImport::Importer::LabelLinksImporter do
end
it 'does not insert label links for non-existing labels' do
+ expect(importer)
+ .to receive(:find_target_id)
+ .and_return(4)
+
expect(importer.label_finder)
.to receive(:id_for)
.with('bug')
@@ -55,6 +59,20 @@ RSpec.describe Gitlab::GithubImport::Importer::LabelLinksImporter do
importer.create_labels
end
+
+ it 'does not insert label links for non-existing targets' do
+ expect(importer)
+ .to receive(:find_target_id)
+ .and_return(nil)
+
+ expect(importer.label_finder)
+ .not_to receive(:id_for)
+
+ expect(LabelLink)
+ .not_to receive(:bulk_insert!)
+
+ importer.create_labels
+ end
end
describe '#find_target_id' do
diff --git a/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb b/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb
index 3377da5ca3a..1d3452a004a 100644
--- a/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb
+++ b/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb
@@ -14,7 +14,8 @@ RSpec.describe ::Gitlab::Nav::TopNavMenuItem, feature_category: :navigation do
view: 'view',
css_class: 'css_class',
data: {},
- partial: 'groups/some_view_partial_file'
+ partial: 'groups/some_view_partial_file',
+ component: '_some_component_used_as_a_trigger_for_frontend_dropdown_item_render_'
}
expect(described_class.build(**item)).to eq(item.merge(type: :item))
diff --git a/spec/migrations/20230223065753_finalize_nullify_creator_id_of_orphaned_projects_spec.rb b/spec/migrations/20230223065753_finalize_nullify_creator_id_of_orphaned_projects_spec.rb
new file mode 100644
index 00000000000..9163c30fe30
--- /dev/null
+++ b/spec/migrations/20230223065753_finalize_nullify_creator_id_of_orphaned_projects_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe FinalizeNullifyCreatorIdOfOrphanedProjects, :migration, feature_category: :projects do
+ let(:batched_migrations) { table(:batched_background_migrations) }
+ let(:batch_failed_status) { 2 }
+ let(:batch_finalized_status) { 3 }
+
+ let!(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ shared_examples 'finalizes the migration' do
+ it 'finalizes the migration' do
+ expect do
+ migrate!
+
+ migration_record.reload
+ failed_job.reload
+ end.to change { migration_record.status }.from(migration_record.status).to(3).and(
+ change { failed_job.status }.from(batch_failed_status).to(batch_finalized_status)
+ )
+ end
+ end
+
+ context 'when migration is missing' do
+ it 'warns migration not found' do
+ expect(Gitlab::AppLogger)
+ .to receive(:warn).with(/Could not find batched background migration for the given configuration:/)
+
+ migrate!
+ end
+ end
+
+ context 'with migration present' do
+ let!(:migration_record) do
+ batched_migrations.create!(
+ job_class_name: 'NullifyCreatorIdColumnOfOrphanedProjects',
+ table_name: :projects,
+ column_name: :id,
+ job_arguments: [],
+ interval: 2.minutes,
+ min_value: 1,
+ max_value: 2,
+ batch_size: 1000,
+ sub_batch_size: 500,
+ max_batch_size: 5000,
+ gitlab_schema: :gitlab_main,
+ status: 3 # finished
+ )
+ end
+
+ context 'when migration finished successfully' do
+ it 'does not raise exception' do
+ expect { migrate! }.not_to raise_error
+ end
+ end
+
+ context 'with different migration statuses', :redis do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :description) do
+ 0 | 'paused'
+ 1 | 'active'
+ 4 | 'failed'
+ 5 | 'finalizing'
+ end
+
+ with_them do
+ let!(:failed_job) do
+ table(:batched_background_migration_jobs).create!(
+ batched_background_migration_id: migration_record.id,
+ status: batch_failed_status,
+ min_value: 1,
+ max_value: 10,
+ attempts: 2,
+ batch_size: 100,
+ sub_batch_size: 10
+ )
+ end
+
+ before do
+ migration_record.update!(status: status)
+ end
+
+ it_behaves_like 'finalizes the migration'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index e29318a7e83..d00f2f29fed 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -70,6 +70,11 @@ RSpec.describe Issue, feature_category: :team_planning do
end
describe 'validations' do
+ it { is_expected.not_to allow_value(nil).for(:confidential) }
+ it { is_expected.to allow_value(true, false).for(:confidential) }
+ end
+
+ describe 'custom validations' do
subject(:valid?) { issue.valid? }
describe 'due_date_after_start_date' do
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index 4b60eaadcbc..078f00334c3 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -1139,6 +1139,15 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['issue_type']).to eq('issue')
end
+ context 'when confidential is null' do
+ it 'responds with 400 error' do
+ post api("/projects/#{project.id}/issues", user), params: { title: 'issue', confidential: nil }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('confidential is empty')
+ end
+ end
+
context 'when issue create service returns an unrecoverable error' do
before do
allow_next_instance_of(Issues::CreateService) do |create_service|
diff --git a/spec/services/resource_access_tokens/create_service_spec.rb b/spec/services/resource_access_tokens/create_service_spec.rb
index 442232920f9..3b6f2bc1368 100644
--- a/spec/services/resource_access_tokens/create_service_spec.rb
+++ b/spec/services/resource_access_tokens/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResourceAccessTokens::CreateService do
+RSpec.describe ResourceAccessTokens::CreateService, feature_category: :system_access do
subject { described_class.new(user, resource, params).execute }
let_it_be(:user) { create(:user) }
@@ -99,6 +99,22 @@ RSpec.describe ResourceAccessTokens::CreateService do
expect(access_token.user.email).to end_with("@noreply.#{Gitlab.config.gitlab.host}")
end
+
+ it 'contains SecureRandom part' do
+ expect(SecureRandom).to receive(:hex).at_least(:once).and_return('randomhex')
+ response = subject
+ access_token = response.payload[:access_token]
+
+ expect(access_token.user.email).to include('_randomhex@noreply')
+ end
+
+ it 'email is the same as username' do
+ expect(SecureRandom).to receive(:hex).at_least(:once).and_return('randomhex')
+ response = subject
+ access_token = response.payload[:access_token]
+
+ expect(access_token.user.email).to include(access_token.user.username)
+ end
end
context 'access level' do
diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb
index 639eb3f2b99..b3378c76658 100644
--- a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb
@@ -84,6 +84,30 @@ RSpec.shared_examples 'User views wiki sidebar' do
expect(page).not_to have_link('View All Pages')
end
+ it 'shows all collapse buttons in the sidebar' do
+ visit wiki_path(wiki)
+
+ within('.right-sidebar') do
+ expect(page.all("[data-testid='chevron-down-icon']").size).to eq(3)
+ end
+ end
+
+ it 'collapses/expands children when click collapse/expand button in the sidebar', :js do
+ visit wiki_path(wiki)
+
+ within('.right-sidebar') do
+ first("[data-testid='chevron-down-icon']").click
+ (11..15).each { |i| expect(page).not_to have_content("my page #{i}") }
+ expect(page.all("[data-testid='chevron-down-icon']").size).to eq(1)
+ expect(page.all("[data-testid='chevron-right-icon']").size).to eq(1)
+
+ first("[data-testid='chevron-right-icon']").click
+ (11..15).each { |i| expect(page).to have_content("my page #{i}") }
+ expect(page.all("[data-testid='chevron-down-icon']").size).to eq(3)
+ expect(page.all("[data-testid='chevron-right-icon']").size).to eq(0)
+ end
+ end
+
context 'when there are more than 15 existing pages' do
before do
create(:wiki_page, wiki: wiki, title: 'my page 16')
diff --git a/spec/views/layouts/header/_new_dropdown.haml_spec.rb b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
index ef8b859c9d9..a547c1be2f4 100644
--- a/spec/views/layouts/header/_new_dropdown.haml_spec.rb
+++ b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
@@ -7,14 +7,15 @@ RSpec.describe 'layouts/header/_new_dropdown', feature_category: :navigation do
shared_examples_for 'invite member selector' do
context 'with ability to invite members' do
- it { is_expected.to have_link('Invite members', href: href) }
+ it { is_expected.to have_selector('.js-invite-members-trigger') }
it { is_expected.to have_selector('.js-invite-members-modal') }
end
context 'without ability to invite members' do
let(:invite_member) { false }
- it { is_expected.not_to have_link('Invite members') }
+ it { is_expected.not_to have_selector('.js-invite-members-trigger') }
+ it { is_expected.not_to have_selector('.js-invite-members-modal') }
end
end
diff --git a/yarn.lock b/yarn.lock
index 84607f089e9..5337bb7cc1b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1355,10 +1355,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.7.3.tgz#9ea641146436da388ffbad25d7f2abe0df52c235"
integrity sha512-NMV++7Ew1FSBDN1xiZaauU9tfeSfgDHcOLpn+8bGpP+O5orUPm2Eu66R5eC5gkjBPaXosNAxNWtriee+aFk4+g==
-"@gitlab/web-ide@0.0.1-dev-20230216131813":
- version "0.0.1-dev-20230216131813"
- resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20230216131813.tgz#f1c5de4e9bea0a1736296b0e63356ca39c065f54"
- integrity sha512-eqGh/gol3vnT62Fs2dR1JWfkX/hVDwcJaYT1JkFPtiaTi2hFwvqw7k3uKL3qIiYzL7V22avxwRIkbAZCI7aKbQ==
+"@gitlab/web-ide@0.0.1-dev-20230223005157":
+ version "0.0.1-dev-20230223005157"
+ resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20230223005157.tgz#e8ef36229dbe25bbfccfc57d5f02cce0c94b53c5"
+ integrity sha512-JLSIC/elod3txZL6CPe6hk07a97VAMjv+DmAvQFAJ/W/cj2wE+AG/ztr/brX2O88Cf4dX4nh48s1n49efMlajQ==
"@graphql-eslint/eslint-plugin@3.12.0":
version "3.12.0"