summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/pages/profiles/saved_replies/index.js3
-rw-r--r--app/assets/javascripts/saved_replies/components/app.vue23
-rw-r--r--app/assets/javascripts/saved_replies/components/list.vue57
-rw-r--r--app/assets/javascripts/saved_replies/components/list_item.vue19
-rw-r--r--app/assets/javascripts/saved_replies/index.js31
-rw-r--r--app/assets/javascripts/saved_replies/pages/index.vue15
-rw-r--r--app/assets/javascripts/saved_replies/queries/saved_replies.query.graphql19
-rw-r--r--app/assets/javascripts/saved_replies/routes.js8
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_discussion.vue2
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue32
-rw-r--r--app/assets/javascripts/work_items/components/work_item_comment_form.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue61
-rw-r--r--app/assets/javascripts/work_items/graphql/notes/create_work_item_note.mutation.graphql (renamed from app/assets/javascripts/work_items/graphql/create_work_item_note.mutation.graphql)2
-rw-r--r--app/assets/javascripts/work_items/graphql/notes/delete_work_item_notes.mutation.graphql7
-rw-r--r--app/assets/javascripts/work_items/graphql/notes/work_item_discussion_note.fragment.graphql (renamed from app/assets/javascripts/work_items/graphql/work_item_discussion_note.fragment.graphql)2
-rw-r--r--app/assets/javascripts/work_items/graphql/notes/work_item_note.fragment.graphql (renamed from app/assets/javascripts/work_items/graphql/work_item_note.fragment.graphql)0
-rw-r--r--app/assets/javascripts/work_items/graphql/notes/work_item_note_created.subscription.graphql (renamed from app/assets/javascripts/work_items/graphql/work_item_note_created.subscription.graphql)2
-rw-r--r--app/assets/javascripts/work_items/graphql/notes/work_item_note_deleted.subscription.graphql (renamed from app/assets/javascripts/work_items/graphql/work_item_note_deleted.subscription.graphql)0
-rw-r--r--app/assets/javascripts/work_items/graphql/notes/work_item_note_updated.subscription.graphql (renamed from app/assets/javascripts/work_items/graphql/work_item_note_updated.subscription.graphql)2
-rw-r--r--app/assets/javascripts/work_items/graphql/notes/work_item_notes.query.graphql (renamed from app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql)2
-rw-r--r--app/assets/javascripts/work_items/graphql/notes/work_item_notes_by_iid.query.graphql (renamed from app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql)2
-rw-r--r--app/assets/javascripts/work_items/utils.js4
-rw-r--r--app/assets/stylesheets/pages/notes.scss5
-rw-r--r--app/controllers/profiles/saved_replies_controller.rb13
-rw-r--r--app/controllers/projects/notes_controller.rb3
-rw-r--r--app/graphql/mutations/ci/job_token_scope/remove_project.rb8
-rw-r--r--app/helpers/users_helper.rb4
-rw-r--r--app/models/concerns/taskable.rb26
-rw-r--r--app/models/namespace/detail.rb2
-rw-r--r--app/presenters/projects/import_export/project_export_presenter.rb4
-rw-r--r--app/serializers/codequality_degradation_entity.rb2
-rw-r--r--app/services/ci/job_token_scope/remove_project_service.rb2
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml12
-rw-r--r--app/views/profiles/saved_replies/index.html.haml10
-rw-r--r--config/feature_flags/development/free_user_cap_over_user_limit_mails.yml8
-rw-r--r--config/feature_flags/development/limited_capacity_seat_refresh_worker_high.yml4
-rw-r--r--config/feature_flags/development/limited_capacity_seat_refresh_worker_low.yml4
-rw-r--r--config/feature_flags/development/limited_capacity_seat_refresh_worker_medium.yml4
-rw-r--r--config/feature_flags/development/use_primary_and_secondary_stores_for_repository_cache.yml8
-rw-r--r--config/feature_flags/development/use_primary_store_as_default_for_repository_cache.yml8
-rw-r--r--config/routes/profile.rb2
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/migrate/20221219112632_add_next_over_limit_check_at_to_namespace_details.rb20
-rw-r--r--db/schema_migrations/202212191126321
-rw-r--r--db/structure.sql3
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/api/group_epic_boards.md4
-rw-r--r--doc/development/contributing/community_roles.md21
-rw-r--r--doc/development/contributing/index.md3
-rw-r--r--doc/integration/advanced_search/elasticsearch.md4
-rw-r--r--doc/integration/jira/development_panel.md4
-rw-r--r--lib/api/entities/issue.rb4
-rw-r--r--lib/feature.rb1
-rw-r--r--lib/gitlab/database/schema_validation/database.rb42
-rw-r--r--lib/gitlab/database/schema_validation/index.rb25
-rw-r--r--lib/gitlab/database/schema_validation/indexes.rb55
-rw-r--r--lib/gitlab/database/schema_validation/structure_sql.rb33
-rw-r--r--lib/gitlab/import_export/group/import_export.yml3
-rw-r--r--lib/gitlab/import_export/project/import_export.yml7
-rw-r--r--lib/gitlab/redis/cache.rb6
-rw-r--r--lib/gitlab/redis/repository_cache.rb12
-rw-r--r--lib/gitlab/regex.rb71
-rw-r--r--lib/gitlab/repository_cache.rb7
-rw-r--r--lib/gitlab/repository_hash_cache.rb7
-rw-r--r--lib/gitlab/repository_set_cache.rb7
-rw-r--r--locale/gitlab.pot20
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb2
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb9
-rw-r--r--spec/features/broadcast_messages_spec.rb3
-rw-r--r--spec/features/issues/user_creates_issue_spec.rb7
-rw-r--r--spec/features/profiles/keys_spec.rb3
-rw-r--r--spec/features/profiles/list_users_saved_replies_spec.rb21
-rw-r--r--spec/features/task_lists_spec.rb72
-rw-r--r--spec/fixtures/api/schemas/entities/codequality_degradation.json5
-rw-r--r--spec/frontend/fixtures/saved_replies.rb46
-rw-r--r--spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap21
-rw-r--r--spec/frontend/saved_replies/components/list_item_spec.js22
-rw-r--r--spec/frontend/saved_replies/components/list_spec.js68
-rw-r--r--spec/frontend/work_items/components/notes/work_item_discussion_spec.js7
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js34
-rw-r--r--spec/frontend/work_items/components/work_item_comment_form_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js100
-rw-r--r--spec/graphql/mutations/ci/job_token_scope/remove_project_spec.rb4
-rw-r--r--spec/lib/gitlab/database/schema_validation/database_spec.rb45
-rw-r--r--spec/lib/gitlab/database/schema_validation/index_spec.rb22
-rw-r--r--spec/lib/gitlab/database/schema_validation/indexes_spec.rb9
-rw-r--r--spec/lib/gitlab/etag_caching/middleware_spec.rb4
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb45
-rw-r--r--spec/lib/gitlab/redis/repository_cache_spec.rb43
-rw-r--r--spec/lib/gitlab/regex_spec.rb69
-rw-r--r--spec/lib/gitlab/repository_cache/preloader_spec.rb89
-rw-r--r--spec/lib/gitlab/repository_hash_cache_spec.rb31
-rw-r--r--spec/models/concerns/taskable_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb9
-rw-r--r--spec/requests/profiles/saved_replies_controller_spec.rb35
-rw-r--r--spec/requests/projects/noteable_notes_spec.rb6
-rw-r--r--spec/serializers/codequality_degradation_entity_spec.rb3
-rw-r--r--spec/services/ci/job_token_scope/remove_project_service_spec.rb2
-rw-r--r--spec/services/projects/import_export/export_service_spec.rb21
-rw-r--r--spec/support/redis.rb6
-rw-r--r--spec/tasks/cache/clear/redis_spec.rb34
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb1
102 files changed, 1313 insertions, 386 deletions
diff --git a/app/assets/javascripts/pages/profiles/saved_replies/index.js b/app/assets/javascripts/pages/profiles/saved_replies/index.js
new file mode 100644
index 00000000000..ef227b82172
--- /dev/null
+++ b/app/assets/javascripts/pages/profiles/saved_replies/index.js
@@ -0,0 +1,3 @@
+import { initSavedReplies } from '~/saved_replies';
+
+initSavedReplies();
diff --git a/app/assets/javascripts/saved_replies/components/app.vue b/app/assets/javascripts/saved_replies/components/app.vue
new file mode 100644
index 00000000000..db8476c44f3
--- /dev/null
+++ b/app/assets/javascripts/saved_replies/components/app.vue
@@ -0,0 +1,23 @@
+<script>
+export default {};
+</script>
+
+<template>
+ <div class="row gl-mt-5">
+ <div class="col-lg-4">
+ <h4 class="gl-mt-0">
+ {{ __('Saved Replies') }}
+ </h4>
+ <p>
+ {{
+ __(
+ 'Saved replies can be used when creating comments inside issues, merge requests, and epics.',
+ )
+ }}
+ </p>
+ </div>
+ <div class="col-lg-8">
+ <router-view />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/saved_replies/components/list.vue b/app/assets/javascripts/saved_replies/components/list.vue
new file mode 100644
index 00000000000..30089cfa53f
--- /dev/null
+++ b/app/assets/javascripts/saved_replies/components/list.vue
@@ -0,0 +1,57 @@
+<script>
+import { GlKeysetPagination, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import savedRepliesQuery from '../queries/saved_replies.query.graphql';
+import ListItem from './list_item.vue';
+
+export default {
+ apollo: {
+ savedReplies: {
+ query: savedRepliesQuery,
+ update: (r) => r.currentUser?.savedReplies?.nodes,
+ result({ data }) {
+ const pageInfo = data.currentUser?.savedReplies?.pageInfo;
+
+ this.count = data.currentUser?.savedReplies?.count;
+
+ if (pageInfo) {
+ this.pageInfo = pageInfo;
+ }
+ },
+ },
+ },
+ components: {
+ GlLoadingIcon,
+ GlKeysetPagination,
+ GlSprintf,
+ ListItem,
+ },
+ data() {
+ return {
+ savedReplies: [],
+ count: 0,
+ pageInfo: {},
+ };
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon v-if="$apollo.queries.savedReplies.loading" size="lg" />
+ <template v-else>
+ <h5 class="gl-font-lg" data-testid="title">
+ <gl-sprintf :message="__('My saved replies (%{count})')">
+ <template #count>{{ count }}</template>
+ </gl-sprintf>
+ </h5>
+ <ul class="gl-list-style-none gl-p-0 gl-m-0">
+ <list-item v-for="reply in savedReplies" :key="reply.id" :reply="reply" />
+ </ul>
+ <gl-keyset-pagination
+ v-if="pageInfo.hasPreviousPage || pageInfo.hasNextPage"
+ v-bind="pageInfo"
+ class="gl-mt-4"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/saved_replies/components/list_item.vue b/app/assets/javascripts/saved_replies/components/list_item.vue
new file mode 100644
index 00000000000..dfa9a405dee
--- /dev/null
+++ b/app/assets/javascripts/saved_replies/components/list_item.vue
@@ -0,0 +1,19 @@
+<script>
+export default {
+ props: {
+ reply: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="gl-mb-5">
+ <div class="gl-display-flex gl-align-items-center">
+ <strong>{{ reply.name }}</strong>
+ </div>
+ <div class="gl-mt-3 gl-font-monospace">{{ reply.content }}</div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/saved_replies/index.js b/app/assets/javascripts/saved_replies/index.js
new file mode 100644
index 00000000000..5022ff62b10
--- /dev/null
+++ b/app/assets/javascripts/saved_replies/index.js
@@ -0,0 +1,31 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import routes from './routes';
+import App from './components/app.vue';
+
+export const initSavedReplies = () => {
+ Vue.use(VueApollo);
+ Vue.use(VueRouter);
+
+ const el = document.getElementById('js-saved-replies-root');
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+ const router = new VueRouter({
+ base: el.dataset.basePath,
+ mode: 'history',
+ routes,
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ router,
+ apolloProvider,
+ render(h) {
+ return h(App);
+ },
+ });
+};
diff --git a/app/assets/javascripts/saved_replies/pages/index.vue b/app/assets/javascripts/saved_replies/pages/index.vue
new file mode 100644
index 00000000000..38f51dbc365
--- /dev/null
+++ b/app/assets/javascripts/saved_replies/pages/index.vue
@@ -0,0 +1,15 @@
+<script>
+import List from '../components/list.vue';
+
+export default {
+ components: {
+ List,
+ },
+};
+</script>
+
+<template>
+ <div>
+ <list />
+ </div>
+</template>
diff --git a/app/assets/javascripts/saved_replies/queries/saved_replies.query.graphql b/app/assets/javascripts/saved_replies/queries/saved_replies.query.graphql
new file mode 100644
index 00000000000..af1f12f3ceb
--- /dev/null
+++ b/app/assets/javascripts/saved_replies/queries/saved_replies.query.graphql
@@ -0,0 +1,19 @@
+query savedReplies {
+ currentUser {
+ id
+ savedReplies {
+ nodes {
+ id
+ name
+ content
+ }
+ count
+ pageInfo {
+ hasNextPage
+ hasPreviousPage
+ endCursor
+ startCursor
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/saved_replies/routes.js b/app/assets/javascripts/saved_replies/routes.js
new file mode 100644
index 00000000000..bd582a5ed86
--- /dev/null
+++ b/app/assets/javascripts/saved_replies/routes.js
@@ -0,0 +1,8 @@
+import IndexComponent from './pages/index.vue';
+
+export default [
+ {
+ path: '/',
+ component: IndexComponent,
+ },
+];
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
index 70253f88795..c6261c99e81 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
@@ -140,6 +140,7 @@ export default {
:note="note"
:discussion-id="discussionId"
@startReplying="showReplyForm"
+ @deleteNote="$emit('deleteNote', note)"
/>
<discussion-notes-replies-wrapper>
<toggle-replies-widget
@@ -155,6 +156,7 @@ export default {
discussion-id="discussionId"
:note="reply"
@startReplying="showReplyForm"
+ @deleteNote="$emit('deleteNote', reply)"
/>
</template>
<work-item-note-replying v-if="isReplying" :body="replyingText" />
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
index 6985e8b3695..a4ffde81a3c 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
@@ -1,5 +1,6 @@
<script>
-import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import { GlAvatarLink, GlAvatar, GlDropdown, GlDropdownItem, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
import NoteHeader from '~/notes/components/note_header.vue';
@@ -8,6 +9,10 @@ import { renderGFM } from '~/behaviors/markdown/render_gfm';
export default {
name: 'WorkItemNoteThread',
+ i18n: {
+ moreActionsText: __('More actions'),
+ deleteNoteText: __('Delete comment'),
+ },
components: {
TimelineEntryItem,
NoteBody,
@@ -15,6 +20,11 @@ export default {
NoteActions,
GlAvatar,
GlAvatarLink,
+ GlDropdown,
+ GlDropdownItem,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
props: {
note: {
@@ -68,6 +78,26 @@ export default {
<div class="note-header">
<note-header :author="author" :created-at="note.createdAt" :note-id="note.id" />
<note-actions :show-reply="showReply" @startReplying="showReplyForm" />
+ <!-- v-if condition should be moved to "delete" dropdown item as soon as we implement copying the link -->
+ <gl-dropdown
+ v-if="note.userPermissions.adminNote"
+ v-gl-tooltip
+ icon="ellipsis_v"
+ text-sr-only
+ right
+ :text="$options.i18n.moreActionsText"
+ :title="$options.i18n.moreActionsText"
+ category="tertiary"
+ no-caret
+ >
+ <gl-dropdown-item
+ variant="danger"
+ data-testid="delete-note-action"
+ @click="$emit('deleteNote')"
+ >
+ {{ $options.i18n.deleteNoteText }}
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
<div class="timeline-discussion-body">
<note-body ref="noteBody" :note="note" />
diff --git a/app/assets/javascripts/work_items/components/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/work_item_comment_form.vue
index 53e704f3da6..3f5a907b88e 100644
--- a/app/assets/javascripts/work_items/components/work_item_comment_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_comment_form.vue
@@ -11,7 +11,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { updateCommentState } from '~/work_items/graphql/cache_utils';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import { getWorkItemQuery } from '../utils';
-import createNoteMutation from '../graphql/create_work_item_note.mutation.graphql';
+import createNoteMutation from '../graphql/notes/create_work_item_note.mutation.graphql';
import { TRACKING_CATEGORY_SHOW, i18n } from '../constants';
import WorkItemNoteSignedOut from './work_item_note_signed_out.vue';
import WorkItemCommentLocked from './work_item_comment_locked.vue';
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
index f4df9012c1d..c42c9be4479 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -1,12 +1,15 @@
<script>
-import { GlSkeletonLoader } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { GlSkeletonLoader, GlModal } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { s__, __ } from '~/locale';
+import { TYPENAME_DISCUSSION, TYPENAME_NOTE } from '~/graphql_shared/constants';
import SystemNote from '~/work_items/components/notes/system_note.vue';
import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
import { i18n, DEFAULT_PAGE_SIZE_NOTES } from '~/work_items/constants';
import { ASC, DESC } from '~/notes/constants';
import { getWorkItemNotesQuery } from '~/work_items/utils';
import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
+import deleteNoteMutation from '../graphql/notes/delete_work_item_notes.mutation.graphql';
import WorkItemCommentForm from './work_item_comment_form.vue';
export default {
@@ -20,6 +23,7 @@ export default {
},
components: {
GlSkeletonLoader,
+ GlModal,
ActivityFilter,
SystemNote,
WorkItemCommentForm,
@@ -53,6 +57,7 @@ export default {
isLoadingMore: false,
perPage: DEFAULT_PAGE_SIZE_NOTES,
sortOrder: ASC,
+ noteToDelete: null,
};
},
computed: {
@@ -173,6 +178,45 @@ export default {
.catch((error) => this.$emit('error', error.message));
this.isLoadingMore = false;
},
+ showDeleteNoteModal(note, discussion) {
+ const isLastNote = discussion.notes.nodes.length === 1;
+ this.$refs.deleteNoteModal.show();
+ this.noteToDelete = { ...note, isLastNote };
+ },
+ cancelDeletingNote() {
+ this.noteToDelete = null;
+ },
+ async deleteNote() {
+ try {
+ const { id, isLastNote, discussion } = this.noteToDelete;
+ await this.$apollo.mutate({
+ mutation: deleteNoteMutation,
+ variables: {
+ input: {
+ id,
+ },
+ },
+ update(cache) {
+ const deletedObject = isLastNote
+ ? { __typename: TYPENAME_DISCUSSION, id: discussion.id }
+ : { __typename: TYPENAME_NOTE, id };
+ cache.modify({
+ id: cache.identify(deletedObject),
+ fields: (_, { DELETE }) => DELETE,
+ });
+ },
+ optimisticResponse: {
+ destroyNote: {
+ note: null,
+ __typename: 'DestroyNotePayload',
+ },
+ },
+ });
+ } catch (error) {
+ this.$emit('error', __('Something went wrong when deleting a comment. Please try again'));
+ Sentry.captureException(error);
+ }
+ },
},
};
</script>
@@ -226,6 +270,7 @@ export default {
:work-item-id="workItemId"
:fetch-by-iid="fetchByIid"
:work-item-type="workItemType"
+ @deleteNote="showDeleteNoteModal($event, discussion)"
/>
</template>
</template>
@@ -251,5 +296,17 @@ export default {
</gl-skeleton-loader>
</template>
</div>
+ <gl-modal
+ ref="deleteNoteModal"
+ modal-id="delete-note-modal"
+ :title="__('Delete comment?')"
+ :ok-title="__('Delete comment')"
+ ok-variant="danger"
+ size="sm"
+ @primary="deleteNote"
+ @canceled="cancelDeletingNote"
+ >
+ {{ __('Are you sure you want to delete this comment?') }}
+ </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/graphql/create_work_item_note.mutation.graphql b/app/assets/javascripts/work_items/graphql/notes/create_work_item_note.mutation.graphql
index 0d7e1aeb089..446ab8e221a 100644
--- a/app/assets/javascripts/work_items/graphql/create_work_item_note.mutation.graphql
+++ b/app/assets/javascripts/work_items/graphql/notes/create_work_item_note.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/work_items/graphql/work_item_note.fragment.graphql"
+#import "./work_item_note.fragment.graphql"
mutation createWorkItemNote($input: CreateNoteInput!) {
createNote(input: $input) {
diff --git a/app/assets/javascripts/work_items/graphql/notes/delete_work_item_notes.mutation.graphql b/app/assets/javascripts/work_items/graphql/notes/delete_work_item_notes.mutation.graphql
new file mode 100644
index 00000000000..592b5c2a991
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/notes/delete_work_item_notes.mutation.graphql
@@ -0,0 +1,7 @@
+mutation deleteWorkItemNote($input: DestroyNoteInput!) {
+ destroyNote(input: $input) {
+ note {
+ id
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_discussion_note.fragment.graphql b/app/assets/javascripts/work_items/graphql/notes/work_item_discussion_note.fragment.graphql
index e5c45ff9751..58561e33e53 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_discussion_note.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/notes/work_item_discussion_note.fragment.graphql
@@ -1,5 +1,5 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
-#import "~/work_items/graphql/work_item_note.fragment.graphql"
+#import "./work_item_note.fragment.graphql"
fragment WorkItemDiscussionNote on Note {
id
diff --git a/app/assets/javascripts/work_items/graphql/work_item_note.fragment.graphql b/app/assets/javascripts/work_items/graphql/notes/work_item_note.fragment.graphql
index d1ab4642869..d1ab4642869 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_note.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/notes/work_item_note.fragment.graphql
diff --git a/app/assets/javascripts/work_items/graphql/work_item_note_created.subscription.graphql b/app/assets/javascripts/work_items/graphql/notes/work_item_note_created.subscription.graphql
index 5591f11823c..739f2101b5e 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_note_created.subscription.graphql
+++ b/app/assets/javascripts/work_items/graphql/notes/work_item_note_created.subscription.graphql
@@ -1,4 +1,4 @@
-#import "~/work_items/graphql/work_item_discussion_note.fragment.graphql"
+#import "./work_item_discussion_note.fragment.graphql"
subscription workItemNoteCreated($noteableId: NoteableID) {
workItemNoteCreated(noteableId: $noteableId) {
diff --git a/app/assets/javascripts/work_items/graphql/work_item_note_deleted.subscription.graphql b/app/assets/javascripts/work_items/graphql/notes/work_item_note_deleted.subscription.graphql
index 6a59becdb99..6a59becdb99 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_note_deleted.subscription.graphql
+++ b/app/assets/javascripts/work_items/graphql/notes/work_item_note_deleted.subscription.graphql
diff --git a/app/assets/javascripts/work_items/graphql/work_item_note_updated.subscription.graphql b/app/assets/javascripts/work_items/graphql/notes/work_item_note_updated.subscription.graphql
index 4a04b311dde..c68d5f491cf 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_note_updated.subscription.graphql
+++ b/app/assets/javascripts/work_items/graphql/notes/work_item_note_updated.subscription.graphql
@@ -1,4 +1,4 @@
-#import "~/work_items/graphql/work_item_note.fragment.graphql"
+#import "./work_item_note.fragment.graphql"
subscription workItemNoteUpdated($noteableId: NoteableID) {
workItemNoteUpdated(noteableId: $noteableId) {
diff --git a/app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql b/app/assets/javascripts/work_items/graphql/notes/work_item_notes.query.graphql
index 9ea9cecc81a..56dc175109f 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/notes/work_item_notes.query.graphql
@@ -1,5 +1,5 @@
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
-#import "~/work_items/graphql/work_item_note.fragment.graphql"
+#import "./work_item_note.fragment.graphql"
query workItemNotes($id: WorkItemID!, $after: String, $pageSize: Int) {
workItem(id: $id) {
diff --git a/app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql b/app/assets/javascripts/work_items/graphql/notes/work_item_notes_by_iid.query.graphql
index f401aa5595e..6b37c68cb43 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/notes/work_item_notes_by_iid.query.graphql
@@ -1,5 +1,5 @@
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
-#import "~/work_items/graphql/work_item_note.fragment.graphql"
+#import "./work_item_note.fragment.graphql"
query workItemNotesByIid($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) {
workspace: project(fullPath: $fullPath) {
diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js
index 3274dab37f3..f2af87d476c 100644
--- a/app/assets/javascripts/work_items/utils.js
+++ b/app/assets/javascripts/work_items/utils.js
@@ -1,8 +1,8 @@
import { WIDGET_TYPE_HIERARCHY } from '~/work_items/constants';
import workItemQuery from './graphql/work_item.query.graphql';
import workItemByIidQuery from './graphql/work_item_by_iid.query.graphql';
-import workItemNotesIdQuery from './graphql/work_item_notes.query.graphql';
-import workItemNotesByIidQuery from './graphql/work_item_notes_by_iid.query.graphql';
+import workItemNotesIdQuery from './graphql/notes/work_item_notes.query.graphql';
+import workItemNotesByIidQuery from './graphql/notes/work_item_notes_by_iid.query.graphql';
export function getWorkItemQuery(isFetchedByIid) {
return isFetchedByIid ? workItemByIidQuery : workItemQuery;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 567f24be336..c6fa1d50997 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -637,6 +637,11 @@ $system-note-svg-size: 1rem;
&.new {
border-right-width: 0;
}
+
+ .note-header {
+ flex-wrap: wrap;
+ align-items: center;
+ }
}
.notes {
diff --git a/app/controllers/profiles/saved_replies_controller.rb b/app/controllers/profiles/saved_replies_controller.rb
new file mode 100644
index 00000000000..5ac5d645efb
--- /dev/null
+++ b/app/controllers/profiles/saved_replies_controller.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Profiles
+ class SavedRepliesController < Profiles::ApplicationController
+ feature_category :user_profile
+
+ before_action do
+ render_404 unless Feature.enabled?(:saved_replies, current_user)
+
+ @hide_search_settings = true
+ end
+ end
+end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 9d3506d49b0..054e8c302c9 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -12,7 +12,8 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
feature_category :team_planning
- urgency :low
+ urgency :medium, [:index]
+ urgency :low, [:create, :update, :destroy, :resolve, :unresolve, :toggle_award_emoji, :outdated_line_change]
def delete_attachment
note.remove_attachment!
diff --git a/app/graphql/mutations/ci/job_token_scope/remove_project.rb b/app/graphql/mutations/ci/job_token_scope/remove_project.rb
index e5f40bbf87e..20e991f5388 100644
--- a/app/graphql/mutations/ci/job_token_scope/remove_project.rb
+++ b/app/graphql/mutations/ci/job_token_scope/remove_project.rb
@@ -24,9 +24,9 @@ module Mutations
description: 'Direction of access, which defaults to outbound.'
field :ci_job_token_scope,
- Types::Ci::JobTokenScopeType,
- null: true,
- description: "CI job token's scope of access."
+ Types::Ci::JobTokenScopeType,
+ null: true,
+ description: "CI job token's scope of access."
def resolve(project_path:, target_project_path:, direction: :outbound)
project = authorized_find!(project_path)
@@ -34,7 +34,7 @@ module Mutations
result = ::Ci::JobTokenScope::RemoveProjectService
.new(project, current_user)
- .execute(target_project, direction: direction)
+ .execute(target_project, direction)
if result.success?
{
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 6fa3fc4f04a..62b9eb2b506 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -300,6 +300,10 @@ module UsersHelper
other: s_('User|Other')
}.with_indifferent_access.freeze
end
+
+ def saved_replies_enabled?
+ Feature.enabled?(:saved_replies, current_user)
+ end
end
UsersHelper.prepend_mod_with('UsersHelper')
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index 05addcf83d2..8297221d5be 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -24,10 +24,28 @@ module Taskable
(\s.+) # followed by whitespace and some text.
}x.freeze
+ # ignore tasks in code or html comment blocks. HTML blocks
+ # are ok as we allow tasks inside <detail> blocks
+ REGEX = %r{
+ #{::Gitlab::Regex.markdown_code_or_html_comment_blocks}
+ |
+ (?<task_item>
+ #{ITEM_PATTERN}
+ )
+ }mx.freeze
+
def self.get_tasks(content)
- content.to_s.scan(ITEM_PATTERN).map do |prefix, checkbox, label|
- TaskList::Item.new("#{prefix} #{checkbox}", label.strip)
+ items = []
+
+ content.to_s.scan(REGEX) do
+ next unless $~[:task_item]
+
+ $~[:task_item].scan(ITEM_PATTERN) do |prefix, checkbox, label|
+ items << TaskList::Item.new("#{prefix.strip} #{checkbox}", label.strip)
+ end
end
+
+ items
end
def self.get_updated_tasks(old_content:, new_content:)
@@ -67,10 +85,10 @@ module Taskable
checklist_item_noun = n_('checklist item', 'checklist items', sum.item_count)
if short
format(s_('Tasks|%{complete_count}/%{total_count} %{checklist_item_noun}'),
-checklist_item_noun: checklist_item_noun, complete_count: sum.complete_count, total_count: sum.item_count)
+ checklist_item_noun: checklist_item_noun, complete_count: sum.complete_count, total_count: sum.item_count)
else
format(s_('Tasks|%{complete_count} of %{total_count} %{checklist_item_noun} completed'),
-checklist_item_noun: checklist_item_noun, complete_count: sum.complete_count, total_count: sum.item_count)
+ checklist_item_noun: checklist_item_noun, complete_count: sum.complete_count, total_count: sum.item_count)
end
end
diff --git a/app/models/namespace/detail.rb b/app/models/namespace/detail.rb
index a5643ab9f79..2660d11171e 100644
--- a/app/models/namespace/detail.rb
+++ b/app/models/namespace/detail.rb
@@ -11,3 +11,5 @@ class Namespace::Detail < ApplicationRecord
self.primary_key = :namespace_id
end
+
+Namespace::Detail.prepend_mod
diff --git a/app/presenters/projects/import_export/project_export_presenter.rb b/app/presenters/projects/import_export/project_export_presenter.rb
index 53c547cde9e..76cc8242da8 100644
--- a/app/presenters/projects/import_export/project_export_presenter.rb
+++ b/app/presenters/projects/import_export/project_export_presenter.rb
@@ -17,7 +17,7 @@ module Projects
delegator_override :project_members
def project_members
- super + converted_group_members
+ super.preload(:user) + converted_group_members # rubocop:disable CodeReuse/ActiveRecord
end
delegator_override :description
@@ -46,7 +46,7 @@ module Projects
# invitee, it would make the following query return 0 rows since a NULL
# user_id would be present in the subquery
non_null_user_ids = project.project_members.connected_to_user.select(:user_id)
- GroupMembersFinder.new(project.group).execute.where.not(user_id: non_null_user_ids)
+ GroupMembersFinder.new(project.group).execute.where.not(user_id: non_null_user_ids).preload(:user)
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/serializers/codequality_degradation_entity.rb b/app/serializers/codequality_degradation_entity.rb
index 52945a753dc..15a26739c51 100644
--- a/app/serializers/codequality_degradation_entity.rb
+++ b/app/serializers/codequality_degradation_entity.rb
@@ -15,4 +15,6 @@ class CodequalityDegradationEntity < Grape::Entity
end
expose :web_url
+
+ expose :engine_name
end
diff --git a/app/services/ci/job_token_scope/remove_project_service.rb b/app/services/ci/job_token_scope/remove_project_service.rb
index d21eff2b619..864f9318c68 100644
--- a/app/services/ci/job_token_scope/remove_project_service.rb
+++ b/app/services/ci/job_token_scope/remove_project_service.rb
@@ -5,7 +5,7 @@ module Ci
class RemoveProjectService < ::BaseService
include EditScopeValidations
- def execute(target_project, direction: :outbound)
+ def execute(target_project, direction)
validate_edit!(project, target_project, current_user)
if project == target_project
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index e1978009114..087eca3ba35 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -130,6 +130,18 @@
= link_to profile_preferences_path do
%strong.fly-out-top-item-name
= _('Preferences')
+ - if saved_replies_enabled?
+ = nav_link(controller: :saved_replies) do
+ = link_to profile_saved_replies_path do
+ .nav-icon-container
+ = sprite_icon('symlink')
+ %span.nav-item-name
+ = _('Saved Replies')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :saved_replies, html_options: { class: "fly-out-top-item" }) do
+ = link_to profile_saved_replies_path do
+ %strong.fly-out-top-item-name
+ = _('Saved Replies')
= nav_link(controller: :active_sessions) do
= link_to profile_active_sessions_path do
.nav-icon-container
diff --git a/app/views/profiles/saved_replies/index.html.haml b/app/views/profiles/saved_replies/index.html.haml
new file mode 100644
index 00000000000..2ae7a092249
--- /dev/null
+++ b/app/views/profiles/saved_replies/index.html.haml
@@ -0,0 +1,10 @@
+- page_title _('Saved Replies')
+
+#js-saved-replies-root.row.gl-mt-5{ data: { base_path: profile_saved_replies_path } }
+ .col-lg-4
+ %h4.gl-mt-0
+ = page_title
+ %p
+ = _('Saved replies can be used when creating comments inside issues, merge requests, and epics.')
+ .col-lg-8
+ = gl_loading_icon(size: 'lg')
diff --git a/config/feature_flags/development/free_user_cap_over_user_limit_mails.yml b/config/feature_flags/development/free_user_cap_over_user_limit_mails.yml
new file mode 100644
index 00000000000..88b3f59050d
--- /dev/null
+++ b/config/feature_flags/development/free_user_cap_over_user_limit_mails.yml
@@ -0,0 +1,8 @@
+---
+name: free_user_cap_over_user_limit_mails
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98438
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/378616
+milestone: '15.9'
+type: development
+group: group::acquisition
+default_enabled: false
diff --git a/config/feature_flags/development/limited_capacity_seat_refresh_worker_high.yml b/config/feature_flags/development/limited_capacity_seat_refresh_worker_high.yml
index def6101ee2f..28f1c8a988e 100644
--- a/config/feature_flags/development/limited_capacity_seat_refresh_worker_high.yml
+++ b/config/feature_flags/development/limited_capacity_seat_refresh_worker_high.yml
@@ -1,7 +1,7 @@
---
name: limited_capacity_seat_refresh_worker_high
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104099/
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/382725"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104099
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/382725
milestone: '15.9'
type: development
group: group::utilization
diff --git a/config/feature_flags/development/limited_capacity_seat_refresh_worker_low.yml b/config/feature_flags/development/limited_capacity_seat_refresh_worker_low.yml
index ac89bf2cd87..a0b306ac792 100644
--- a/config/feature_flags/development/limited_capacity_seat_refresh_worker_low.yml
+++ b/config/feature_flags/development/limited_capacity_seat_refresh_worker_low.yml
@@ -1,7 +1,7 @@
---
name: limited_capacity_seat_refresh_worker_low
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104099/
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/382725"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104099
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/382725
milestone: '15.9'
type: development
group: group::utilization
diff --git a/config/feature_flags/development/limited_capacity_seat_refresh_worker_medium.yml b/config/feature_flags/development/limited_capacity_seat_refresh_worker_medium.yml
index 1169d3fdc2d..1df482e0624 100644
--- a/config/feature_flags/development/limited_capacity_seat_refresh_worker_medium.yml
+++ b/config/feature_flags/development/limited_capacity_seat_refresh_worker_medium.yml
@@ -1,7 +1,7 @@
---
name: limited_capacity_seat_refresh_worker_medium
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104099/
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/382725"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104099
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/382725
milestone: '15.9'
type: development
group: group::utilization
diff --git a/config/feature_flags/development/use_primary_and_secondary_stores_for_repository_cache.yml b/config/feature_flags/development/use_primary_and_secondary_stores_for_repository_cache.yml
deleted file mode 100644
index 07fbc77c960..00000000000
--- a/config/feature_flags/development/use_primary_and_secondary_stores_for_repository_cache.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: use_primary_and_secondary_stores_for_repository_cache
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107232#note_1216317991
-rollout_issue_url:
-milestone: '15.7'
-type: development
-group: group::scalability
-default_enabled: false
diff --git a/config/feature_flags/development/use_primary_store_as_default_for_repository_cache.yml b/config/feature_flags/development/use_primary_store_as_default_for_repository_cache.yml
deleted file mode 100644
index adf48021597..00000000000
--- a/config/feature_flags/development/use_primary_store_as_default_for_repository_cache.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: use_primary_store_as_default_for_repository_cache
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107232#note_1216317991
-rollout_issue_url:
-milestone: '15.7'
-type: development
-group: group::scalability
-default_enabled: false
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index 46a078ce3b1..bee1a0f108e 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -39,6 +39,8 @@ resource :profile, only: [:show, :update] do
end
resource :preferences, only: [:show, :update]
+ resources :saved_replies, only: [:index], action: :index
+
resources :keys, only: [:index, :show, :create, :destroy] do
member do
delete :revoke
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 2c8169e423e..cad453fe1de 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -337,6 +337,8 @@
- 1
- - migrate_external_diffs
- 1
+- - namespaces_free_user_cap_over_limit_notification
+ - 1
- - namespaces_process_sync_events
- 1
- - namespaces_sync_namespace_name
diff --git a/db/migrate/20221219112632_add_next_over_limit_check_at_to_namespace_details.rb b/db/migrate/20221219112632_add_next_over_limit_check_at_to_namespace_details.rb
new file mode 100644
index 00000000000..dd2acbfd0bb
--- /dev/null
+++ b/db/migrate/20221219112632_add_next_over_limit_check_at_to_namespace_details.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddNextOverLimitCheckAtToNamespaceDetails < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ TABLE_NAME = :namespace_details
+ COLUMN = :next_over_limit_check_at
+
+ def up
+ with_lock_retries do
+ add_column TABLE_NAME, COLUMN, :datetime_with_timezone, null: true
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_column TABLE_NAME, COLUMN
+ end
+ end
+end
diff --git a/db/schema_migrations/20221219112632 b/db/schema_migrations/20221219112632
new file mode 100644
index 00000000000..0bba0080af7
--- /dev/null
+++ b/db/schema_migrations/20221219112632
@@ -0,0 +1 @@
+400cab0a2d3130dd7406024cf982c7312918019197ae06af06696435f6bb5aaa \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 005df75da1f..067e8646819 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -18310,7 +18310,8 @@ CREATE TABLE namespace_details (
free_user_cap_over_limt_notified_at timestamp with time zone,
free_user_cap_over_limit_notified_at timestamp with time zone,
dashboard_notification_at timestamp with time zone,
- dashboard_enforcement_at timestamp with time zone
+ dashboard_enforcement_at timestamp with time zone,
+ next_over_limit_check_at timestamp with time zone
);
CREATE TABLE namespace_limits (
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 9bf303234f4..6a0f731f6ad 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -11666,6 +11666,7 @@ Represents a code quality degradation on the pipeline.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="codequalitydegradationdescription"></a>`description` | [`String!`](#string) | Description of the code quality degradation. |
+| <a id="codequalitydegradationenginename"></a>`engineName` | [`String!`](#string) | Code Quality plugin that reported the finding. |
| <a id="codequalitydegradationfingerprint"></a>`fingerprint` | [`String!`](#string) | Unique fingerprint to identify the code quality degradation. For example, an MD5 hash. |
| <a id="codequalitydegradationline"></a>`line` | [`Int!`](#int) | Line on which the code quality degradation occurred. |
| <a id="codequalitydegradationpath"></a>`path` | [`String!`](#string) | Relative path to the file containing the code quality degradation. |
diff --git a/doc/api/group_epic_boards.md b/doc/api/group_epic_boards.md
index bb3d2a8acf4..e85147a2868 100644
--- a/doc/api/group_epic_boards.md
+++ b/doc/api/group_epic_boards.md
@@ -178,6 +178,8 @@ Example response:
## List group epic board lists
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/385904) in GitLab 15.9.
+
Gets a list of the epic board's lists.
Does not include `open` and `closed` lists.
@@ -236,6 +238,8 @@ Example response:
## Single group epic board list
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/385904) in GitLab 15.9.
+
Gets a single board list.
```plaintext
diff --git a/doc/development/contributing/community_roles.md b/doc/development/contributing/community_roles.md
index 8aa219d72a1..3c9362138c2 100644
--- a/doc/development/contributing/community_roles.md
+++ b/doc/development/contributing/community_roles.md
@@ -1,18 +1,11 @@
---
-stage: none
-group: Development
-info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+redirect_to: 'index.md'
+remove_date: '2023-05-08'
---
-# Community members & roles
+This document was moved to [another location](index.md).
-GitLab community members and their privileges/responsibilities.
-
-| Roles | Responsibilities | Requirements |
-|-------|------------------|--------------|
-| Maintainer | Accepts merge requests on several GitLab projects | Added to the [team page](https://about.gitlab.com/company/team/). An expert on code reviews and knows the product/codebase |
-| Reviewer | Performs code reviews on MRs | Added to the [team page](https://about.gitlab.com/company/team/) |
-| Developer | Has access to GitLab internal infrastructure & issues (for example, HR-related) | GitLab employee or a Core Team member (with an NDA) |
-| Contributor | Can make contributions to all GitLab public projects | Have a GitLab.com account |
-
-[List of current reviewers/maintainers](https://about.gitlab.com/handbook/engineering/projects/#gitlab).
+<!-- This redirect file can be deleted after <2023-05-08>. -->
+<!-- Redirects that point to other docs in the same project expire in three months. -->
+<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
diff --git a/doc/development/contributing/index.md b/doc/development/contributing/index.md
index 0a5160b6569..5feabf2bd18 100644
--- a/doc/development/contributing/index.md
+++ b/doc/development/contributing/index.md
@@ -23,9 +23,6 @@ GitLab comes in two flavors:
Throughout this guide you will see references to CE and EE for abbreviation.
-To get an overview of GitLab community membership, including those that would review or merge
-your contributions, visit [the community roles page](community_roles.md).
-
## Code of conduct
We want to create a welcoming environment for everyone who is interested in contributing.
diff --git a/doc/integration/advanced_search/elasticsearch.md b/doc/integration/advanced_search/elasticsearch.md
index 09c7873922d..f1c2ad8ea6b 100644
--- a/doc/integration/advanced_search/elasticsearch.md
+++ b/doc/integration/advanced_search/elasticsearch.md
@@ -588,7 +588,7 @@ The following are some available Rake tasks:
| Task | Description |
|:--------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`sudo gitlab-rake gitlab:elastic:info`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Outputs debugging information for the Advanced Search integration. |
-| [`sudo gitlab-rake gitlab:elastic:index`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Enables Elasticsearch indexing and run `gitlab:elastic:create_empty_index`, `gitlab:elastic:clear_index_status`, `gitlab:elastic:index_projects`, and `gitlab:elastic:index_snippets`. |
+| [`sudo gitlab-rake gitlab:elastic:index`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Enables Elasticsearch indexing and run `gitlab:elastic:create_empty_index`, `gitlab:elastic:clear_index_status`, `gitlab:elastic:index_projects`, `gitlab:elastic:index_snippets`, and `gitlab:elastic:index_users`. |
| [`sudo gitlab-rake gitlab:elastic:pause_indexing`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Pauses Elasticsearch indexing. Changes are still tracked. Useful for cluster/index migrations. |
| [`sudo gitlab-rake gitlab:elastic:resume_indexing`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Resumes Elasticsearch indexing. |
| [`sudo gitlab-rake gitlab:elastic:index_projects`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Iterates over all projects, and queues Sidekiq jobs to index them in the background. It can only be used after the index is created. |
@@ -598,6 +598,7 @@ The following are some available Rake tasks:
| [`sudo gitlab-rake gitlab:elastic:delete_index`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Removes the GitLab indices and aliases (if they exist) on the Elasticsearch instance. |
| [`sudo gitlab-rake gitlab:elastic:recreate_index`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Wrapper task for `gitlab:elastic:delete_index` and `gitlab:elastic:create_empty_index`. |
| [`sudo gitlab-rake gitlab:elastic:index_snippets`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Performs an Elasticsearch import that indexes the snippets data. |
+| [`sudo gitlab-rake gitlab:elastic:index_users`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Imports all users into Elasticsearch. |
| [`sudo gitlab-rake gitlab:elastic:projects_not_indexed`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Displays which projects are not indexed. |
| [`sudo gitlab-rake gitlab:elastic:reindex_cluster`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Schedules a zero-downtime cluster reindexing task. This feature should be used with an index that was created after GitLab 13.0. |
| [`sudo gitlab-rake gitlab:elastic:mark_reindex_failed`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Mark the most recent re-index job as failed. |
@@ -647,6 +648,7 @@ When performing a search, the GitLab index uses the following scopes:
| `notes` | Note data |
| `snippets` | Snippet data |
| `wiki_blobs` | Wiki contents |
+| `users` | Users |
## Tuning
diff --git a/doc/integration/jira/development_panel.md b/doc/integration/jira/development_panel.md
index f72b714550c..f36b9f2c4c1 100644
--- a/doc/integration/jira/development_panel.md
+++ b/doc/integration/jira/development_panel.md
@@ -69,8 +69,8 @@ To simplify administration, we recommend that a GitLab group maintainer or group
| Jira usage | GitLab.com customers need | GitLab self-managed customers need |
|------------|---------------------------|------------------------------------|
-| [Atlassian cloud](https://www.atlassian.com/migration/assess/why-cloud) | The [GitLab for Jira Cloud app](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview) from the [Atlassian Marketplace](https://marketplace.atlassian.com). This method offers real-time sync between GitLab.com and Jira. For more information, see [GitLab for Jira Cloud app](connect-app.md). | The GitLab for Jira Cloud app [installed manually](connect-app.md#install-the-gitlab-for-jira-cloud-app-manually). By default, you can install the app from the [Atlassian Marketplace](https://marketplace.atlassian.com/). For more information, see [Connect the GitLab for Jira Cloud app for self-managed instances](connect-app.md#connect-the-gitlab-for-jira-cloud-app-for-self-managed-instances). |
-| Your own server | The [Jira DVCS (distributed version control system) connector](dvcs/index.md). This syncs data hourly. | The [Jira DVCS (distributed version control system) connector](dvcs/index.md). This syncs data hourly. |
+| [Atlassian cloud](https://www.atlassian.com/migration/assess/why-cloud) | The [GitLab for Jira Cloud app](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview) from the [Atlassian Marketplace](https://marketplace.atlassian.com). This method offers real-time sync between GitLab.com and Jira. The method requires inbound connections for the setup and then pushes data to Jira through outbound connections. For more information, see [GitLab for Jira Cloud app](connect-app.md). | The GitLab for Jira Cloud app [installed manually](connect-app.md#install-the-gitlab-for-jira-cloud-app-manually). By default, you can install the app from the [Atlassian Marketplace](https://marketplace.atlassian.com/). The method requires inbound connections for the setup and then pushes data to Jira through outbound connections. For more information, see [Connect the GitLab for Jira Cloud app for self-managed instances](connect-app.md#connect-the-gitlab-for-jira-cloud-app-for-self-managed-instances). |
+| Your own server | The [Jira DVCS connector](dvcs/index.md). This method syncs data every hour and works only with inbound connections. The method tries to set up webhooks in GitLab to implement real-time data sync, which does not work without outbound connections. | The [Jira DVCS connector](dvcs/index.md). This method syncs data every hour and works only with inbound connections. The method tries to set up webhooks in GitLab to implement real-time data sync, which does not work without outbound connections. |
Each GitLab project can be configured to connect to an entire Jira instance. That means after
configuration, one GitLab project can interact with all Jira projects in that instance. For:
diff --git a/lib/api/entities/issue.rb b/lib/api/entities/issue.rb
index 7630fd1e94e..56e942a0383 100644
--- a/lib/api/entities/issue.rb
+++ b/lib/api/entities/issue.rb
@@ -6,11 +6,11 @@ module API
include ::API::Helpers::RelatedResourcesHelpers
expose(:has_tasks) do |issue, _|
- !issue.task_list_items.empty?
+ !issue.tasks?
end
expose :task_status, if: -> (issue, _) do
- !issue.task_list_items.empty?
+ !issue.tasks?
end
expose :_links do
diff --git a/lib/feature.rb b/lib/feature.rb
index 892d32c9b73..17c26796ea1 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -424,4 +424,5 @@ module Feature
end
end
+Feature.prepend_mod
Feature::ActiveSupportCacheStoreAdapter.prepend_mod_with('Feature::ActiveSupportCacheStoreAdapter')
diff --git a/lib/gitlab/database/schema_validation/database.rb b/lib/gitlab/database/schema_validation/database.rb
new file mode 100644
index 00000000000..dfc845f0b44
--- /dev/null
+++ b/lib/gitlab/database/schema_validation/database.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module SchemaValidation
+ class Database
+ def initialize(connection)
+ @connection = connection
+ end
+
+ def fetch_index_by_name(index_name)
+ index_map[index_name]
+ end
+
+ def indexes
+ index_map.values
+ end
+
+ private
+
+ def index_map
+ @index_map ||=
+ fetch_indexes.transform_values! do |index_stmt|
+ Index.new(PgQuery.parse(index_stmt).tree.stmts.first.stmt.index_stmt)
+ end
+ end
+
+ attr_reader :connection
+
+ def fetch_indexes
+ sql = <<~SQL
+ SELECT indexname, indexdef
+ FROM pg_indexes
+ WHERE indexname NOT LIKE '%_pkey' AND schemaname IN ('public', 'gitlab_partitions_static');
+ SQL
+
+ @fetch_indexes ||= connection.exec_query(sql).rows.to_h
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/schema_validation/index.rb b/lib/gitlab/database/schema_validation/index.rb
new file mode 100644
index 00000000000..af0d5f31f4e
--- /dev/null
+++ b/lib/gitlab/database/schema_validation/index.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module SchemaValidation
+ class Index
+ def initialize(parsed_stmt)
+ @parsed_stmt = parsed_stmt
+ end
+
+ def name
+ parsed_stmt.idxname
+ end
+
+ def statement
+ @statement ||= PgQuery.deparse_stmt(parsed_stmt)
+ end
+
+ private
+
+ attr_reader :parsed_stmt
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/schema_validation/indexes.rb b/lib/gitlab/database/schema_validation/indexes.rb
index 82a9b39790d..b7c3705bde9 100644
--- a/lib/gitlab/database/schema_validation/indexes.rb
+++ b/lib/gitlab/database/schema_validation/indexes.rb
@@ -4,68 +4,33 @@ module Gitlab
module Database
module SchemaValidation
class Indexes
- def initialize(structure_file_path, database_name)
- @parsed_structure_file = PgQuery.parse(File.read(structure_file_path))
- @database_name = database_name
+ def initialize(structure_sql, database)
+ @structure_sql = structure_sql
+ @database = database
end
def missing_indexes
- structure_file_indexes.keys - database_indexes.keys
+ structure_sql.indexes.map(&:name) - database.indexes.map(&:name)
end
def extra_indexes
- database_indexes.keys - structure_file_indexes.keys
+ database.indexes.map(&:name) - structure_sql.indexes.map(&:name)
end
def wrong_indexes
- structure_file_indexes.filter_map do |index_name, index_stmt|
- database_index = database_indexes[index_name]
+ structure_sql.indexes.filter_map do |structure_sql_index|
+ database_index = database.fetch_index_by_name(structure_sql_index.name)
next if database_index.nil?
+ next if database_index.statement == structure_sql_index.statement
- begin
- database_index = PgQuery.deparse_stmt(PgQuery.parse(database_index).tree.stmts.first.stmt.index_stmt)
-
- index_stmt.relation.schemaname = "public" if index_stmt.relation.schemaname == ''
-
- structure_sql_index = PgQuery.deparse_stmt(index_stmt)
-
- index_name unless database_index == structure_sql_index
- rescue PgQuery::ParseError
- index_name
- end
+ structure_sql_index.name
end
end
private
- attr_reader :parsed_structure_file, :database_name
-
- def structure_file_indexes
- @structure_file_indexes ||= index_parsed_structure_file.each_with_object({}) do |tree, dic|
- index_stmt = tree.stmt.index_stmt
-
- dic[index_stmt.idxname] = index_stmt
- end
- end
-
- def index_parsed_structure_file
- @index_parsed_structure_file ||= parsed_structure_file.tree.stmts.reject { |s| s.stmt.index_stmt.nil? }
- end
-
- def database_indexes
- sql = <<~SQL
- SELECT indexname, indexdef
- FROM pg_indexes
- WHERE indexname NOT LIKE '%_pkey' AND schemaname IN ('public', 'gitlab_partitions_static');
- SQL
-
- @database_indexes ||= connection.exec_query(sql).rows.to_h
- end
-
- def connection
- @connection ||= Gitlab::Database.database_base_models[database_name].connection
- end
+ attr_reader :structure_sql, :database
end
end
end
diff --git a/lib/gitlab/database/schema_validation/structure_sql.rb b/lib/gitlab/database/schema_validation/structure_sql.rb
new file mode 100644
index 00000000000..32c69a0e5e7
--- /dev/null
+++ b/lib/gitlab/database/schema_validation/structure_sql.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module SchemaValidation
+ class StructureSql
+ def initialize(structure_file_path)
+ @structure_file_path = structure_file_path
+ end
+
+ def indexes
+ @indexes ||= index_statements.map do |index_statement|
+ index_statement.relation.schemaname = "public" if index_statement.relation.schemaname == ''
+
+ Index.new(index_statement)
+ end
+ end
+
+ private
+
+ attr_reader :structure_file_path
+
+ def index_statements
+ parsed_structure_file.tree.stmts.filter_map { |s| s.stmt.index_stmt }
+ end
+
+ def parsed_structure_file
+ PgQuery.parse(File.read(structure_file_path))
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/group/import_export.yml b/lib/gitlab/import_export/group/import_export.yml
index 7f3254be3e8..e30414265be 100644
--- a/lib/gitlab/import_export/group/import_export.yml
+++ b/lib/gitlab/import_export/group/import_export.yml
@@ -98,6 +98,9 @@ methods:
epics:
- :state
+# Add in this list the nested associations that are used to export the parent
+# association, but are not present in the tree list. In other words, the associations
+# that needs to be preloaded but do not need to be exported.
preloads:
export_reorders:
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index 7c24bf95695..d97ffee8698 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -1105,6 +1105,9 @@ methods:
issues:
- :state
+# Add in this list the nested associations that are used to export the parent
+# association, but are not present in the tree list. In other words, the associations
+# that needs to be preloaded but do not need to be exported.
preloads:
issues:
project: :route
@@ -1113,8 +1116,8 @@ preloads:
# tags: # needed by tag_list
project: # deprecated: needed by coverage_regex of Ci::Build
merge_requests:
- source_project: # needed by source_branch_sha and diff_head_sha
- target_project: # needed by target_branch_sha
+ source_project: :route # needed by source_branch_sha and diff_head_sha
+ target_project: :route # needed by target_branch_sha
assignees: # needed by assigne_id that is implemented by DeprecatedAssignee
# Specify a custom export reordering for a given relationship
diff --git a/lib/gitlab/redis/cache.rb b/lib/gitlab/redis/cache.rb
index 043f14630d5..ba3af3e7a6f 100644
--- a/lib/gitlab/redis/cache.rb
+++ b/lib/gitlab/redis/cache.rb
@@ -12,9 +12,13 @@ module Gitlab
redis: pool,
compress: Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1')),
namespace: CACHE_NAMESPACE,
- expires_in: ENV.fetch('GITLAB_RAILS_CACHE_DEFAULT_TTL_SECONDS', 8.hours).to_i # Cache should not grow forever
+ expires_in: default_ttl_seconds
}
end
+
+ def self.default_ttl_seconds
+ ENV.fetch('GITLAB_RAILS_CACHE_DEFAULT_TTL_SECONDS', 8.hours).to_i
+ end
end
end
end
diff --git a/lib/gitlab/redis/repository_cache.rb b/lib/gitlab/redis/repository_cache.rb
index 8bfbfcfea60..d8d434ae062 100644
--- a/lib/gitlab/redis/repository_cache.rb
+++ b/lib/gitlab/redis/repository_cache.rb
@@ -14,19 +14,9 @@ module Gitlab
redis: pool,
compress: Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1')),
namespace: Cache::CACHE_NAMESPACE,
- # Cache should not grow forever
- expires_in: ENV.fetch('GITLAB_RAILS_CACHE_DEFAULT_TTL_SECONDS', 8.hours).to_i
+ expires_in: Cache.default_ttl_seconds
)
end
-
- private
-
- def redis
- primary_store = ::Redis.new(params)
- secondary_store = ::Redis.new(config_fallback.params)
-
- MultiStore.new(primary_store, secondary_store, store_name)
- end
end
end
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 717bd22d81d..93f663d7030 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -435,30 +435,59 @@ module Gitlab
}x.freeze
end
+ MARKDOWN_CODE_BLOCK_REGEX = %r{
+ (?<code>
+ # Code blocks:
+ # ```
+ # Anything, including `>>>` blocks which are ignored by this filter
+ # ```
+
+ ^```
+ .+?
+ \n```\ *$
+ )
+ }mx.freeze
+
+ MARKDOWN_HTML_BLOCK_REGEX = %r{
+ (?<html>
+ # HTML block:
+ # <tag>
+ # Anything, including `>>>` blocks which are ignored by this filter
+ # </tag>
+
+ ^<[^>]+?>\ *\n
+ .+?
+ \n<\/[^>]+?>\ *$
+ )
+ }mx.freeze
+
+ MARKDOWN_HTML_COMMENT_BLOCK_REGEX = %r{
+ (?<html_block_comment>
+ # HTML block comment:
+ # <!-- some comment text
+ # more comment
+ # and more comment -->
+
+ ^<!--.*?\ *\n
+ .+?
+ \n.*?-->\ *$
+ )
+ }mx.freeze
+
def markdown_code_or_html_blocks
@markdown_code_or_html_blocks ||= %r{
- (?<code>
- # Code blocks:
- # ```
- # Anything, including `>>>` blocks which are ignored by this filter
- # ```
-
- ^```
- .+?
- \n```\ *$
- )
+ #{MARKDOWN_CODE_BLOCK_REGEX}
|
- (?<html>
- # HTML block:
- # <tag>
- # Anything, including `>>>` blocks which are ignored by this filter
- # </tag>
-
- ^<[^>]+?>\ *\n
- .+?
- \n<\/[^>]+?>\ *$
- )
- }mx
+ #{MARKDOWN_HTML_BLOCK_REGEX}
+ }mx.freeze
+ end
+
+ def markdown_code_or_html_comment_blocks
+ @markdown_code_or_html_comment_blocks ||= %r{
+ #{MARKDOWN_CODE_BLOCK_REGEX}
+ |
+ #{MARKDOWN_HTML_COMMENT_BLOCK_REGEX}
+ }mx.freeze
end
# Based on Jira's project key format
diff --git a/lib/gitlab/repository_cache.rb b/lib/gitlab/repository_cache.rb
index 8de2c2fe772..498eaf92381 100644
--- a/lib/gitlab/repository_cache.rb
+++ b/lib/gitlab/repository_cache.rb
@@ -50,12 +50,7 @@ module Gitlab
end
def self.store
- if Feature.enabled?(:use_primary_and_secondary_stores_for_repository_cache) ||
- Feature.enabled?(:use_primary_store_as_default_for_repository_cache)
- Gitlab::Redis::RepositoryCache.cache_store
- else
- Rails.cache
- end
+ Gitlab::Redis::RepositoryCache.cache_store
end
end
end
diff --git a/lib/gitlab/repository_hash_cache.rb b/lib/gitlab/repository_hash_cache.rb
index ea90a341b1e..1f3c084e194 100644
--- a/lib/gitlab/repository_hash_cache.rb
+++ b/lib/gitlab/repository_hash_cache.rb
@@ -140,12 +140,7 @@ module Gitlab
private
def cache
- if Feature.enabled?(:use_primary_and_secondary_stores_for_repository_cache) ||
- Feature.enabled?(:use_primary_store_as_default_for_repository_cache)
- Gitlab::Redis::RepositoryCache
- else
- Gitlab::Redis::Cache
- end
+ Gitlab::Redis::RepositoryCache
end
def with(&blk)
diff --git a/lib/gitlab/repository_set_cache.rb b/lib/gitlab/repository_set_cache.rb
index c67ca92af40..838f44c0f9e 100644
--- a/lib/gitlab/repository_set_cache.rb
+++ b/lib/gitlab/repository_set_cache.rb
@@ -68,12 +68,7 @@ module Gitlab
private
def cache
- if Feature.enabled?(:use_primary_and_secondary_stores_for_repository_cache) ||
- Feature.enabled?(:use_primary_store_as_default_for_repository_cache)
- Gitlab::Redis::RepositoryCache
- else
- Gitlab::Redis::Cache
- end
+ Gitlab::Redis::RepositoryCache
end
def with(&blk)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 84c3d7ce4c2..f4f1fd3fb24 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6525,6 +6525,9 @@ msgstr ""
msgid "BillingPlans|Ultimate"
msgstr ""
+msgid "BillingPlans|Upgrade"
+msgstr ""
+
msgid "BillingPlans|Upgrade to Premium"
msgstr ""
@@ -13377,6 +13380,9 @@ msgstr ""
msgid "Delete comment"
msgstr ""
+msgid "Delete comment?"
+msgstr ""
+
msgid "Delete corpus"
msgstr ""
@@ -17970,7 +17976,7 @@ msgstr ""
msgid "FreeUserCap|Explore paid plans:"
msgstr ""
-msgid "FreeUserCap|Looks like you've reached your limit of %{free_user_limit} members for \"%{namespace_name}\". You can't add any more, but you can manage your existing members, for example, by removing inactive members and replacing them with new members."
+msgid "FreeUserCap|It looks like you've reached your limit of %{free_user_limit} members for \"%{namespace_name}\", according to the check we ran on %{date_time}. You can't add any more, but you can manage your existing members, for example, by removing inactive members and replacing them with new members."
msgstr ""
msgid "FreeUserCap|Manage members"
@@ -27564,6 +27570,9 @@ msgstr ""
msgid "My company or team"
msgstr ""
+msgid "My saved replies (%{count})"
+msgstr ""
+
msgid "My topic"
msgstr ""
@@ -37339,6 +37348,12 @@ msgstr ""
msgid "Save pipeline schedule"
msgstr ""
+msgid "Saved Replies"
+msgstr ""
+
+msgid "Saved replies can be used when creating comments inside issues, merge requests, and epics."
+msgstr ""
+
msgid "Saving"
msgstr ""
@@ -40161,6 +40176,9 @@ msgstr ""
msgid "Something went wrong trying to load issue contacts."
msgstr ""
+msgid "Something went wrong when deleting a comment. Please try again"
+msgstr ""
+
msgid "Something went wrong when reordering designs. Please try again"
msgstr ""
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 0afd2e10ea2..23b0b58158f 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::NotesController do
+RSpec.describe Projects::NotesController, type: :controller, feature_category: :team_planning do
include ProjectForksHelper
let(:user) { create(:user) }
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 00866ca118f..4e0c098ad81 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -68,14 +68,7 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
check_pipeline_response(returned: 2, all: 6)
end
- context 'when performing gitaly calls', :request_store do
- before do
- # To prevent double writes / fallback read due to MultiStore which is failing the `Gitlab::GitalyClient
- # .get_request_count` expectation.
- stub_feature_flags(use_primary_store_as_default_for_repository_cache: false)
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false)
- end
-
+ context 'when performing gitaly calls', :request_store, :use_null_store_as_repository_cache do
it 'limits the Gitaly requests' do
# Isolate from test preparation (Repository#exists? is also cached in RequestStore)
RequestStore.end!
diff --git a/spec/features/broadcast_messages_spec.rb b/spec/features/broadcast_messages_spec.rb
index 8300cfce539..b2b41d653b6 100644
--- a/spec/features/broadcast_messages_spec.rb
+++ b/spec/features/broadcast_messages_spec.rb
@@ -23,7 +23,8 @@ RSpec.describe 'Broadcast Messages', feature_category: :onboarding do
end
shared_examples 'a dismissable Broadcast Messages' do
- it 'hides broadcast message after dismiss', :js do
+ it 'hides broadcast message after dismiss', :js,
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/390900' do
visit root_path
find('.js-dismiss-current-broadcast-notification').click
diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb
index df039493cec..c5d0791dc57 100644
--- a/spec/features/issues/user_creates_issue_spec.rb
+++ b/spec/features/issues/user_creates_issue_spec.rb
@@ -157,15 +157,10 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
end
end
- context 'form filled by URL parameters' do
+ context 'form filled by URL parameters', :use_null_store_as_repository_cache do
let(:project) { create(:project, :public, :repository) }
before do
- # With multistore feature flags enabled (using an actual Redis store instead of NullStore),
- # it somehow writes an invalid content to Redis and the specs would fail.
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false)
- stub_feature_flags(use_primary_store_as_default_for_repository_cache: false)
-
project.repository.create_file(
user,
'.gitlab/issue_templates/bug.md',
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index 8d4666dcb50..c2f67f36850 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -122,7 +122,8 @@ RSpec.describe 'Profile > SSH Keys', feature_category: :user_profile do
project.add_developer(user)
end
- it 'revoking the SSH key marks commits as unverified' do
+ it 'revoking the SSH key marks commits as unverified',
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/390905' do
visit project_commit_path(project, commit)
find('a.gpg-status-box', text: 'Verified').click
diff --git a/spec/features/profiles/list_users_saved_replies_spec.rb b/spec/features/profiles/list_users_saved_replies_spec.rb
new file mode 100644
index 00000000000..4f3678f8051
--- /dev/null
+++ b/spec/features/profiles/list_users_saved_replies_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Profile > Notifications > List users saved replies', :js,
+ feature_category: :user_profile do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:saved_reply) { create(:saved_reply, user: user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'shows the user a list of their saved replies' do
+ visit profile_saved_replies_path
+
+ expect(page).to have_content('My saved replies (1)')
+ expect(page).to have_content(saved_reply.name)
+ expect(page).to have_content(saved_reply.content)
+ end
+end
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index d35726fe125..95741d6cdf0 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -333,6 +333,41 @@ RSpec.describe 'Task Lists', :js, feature_category: :team_planning do
expect(page).to have_selector('ul.task-list', count: 1)
expect(page).to have_selector('li.task-list-item', count: 1)
expect(page).to have_selector('ul input[checked]', count: 1)
+ expect(page).to have_content('1 of 1 checklist item completed')
+ end
+ end
+
+ describe 'tasks in code blocks' do
+ let(:code_tasks_markdown) do
+ <<-EOT.strip_heredoc
+ ```
+ - [ ] a
+ ```
+
+ - [ ] b
+ EOT
+ end
+
+ let!(:issue) { create(:issue, description: code_tasks_markdown, author: user, project: project) }
+
+ it 'renders' do
+ visit_issue(project, issue)
+ wait_for_requests
+
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 1)
+ expect(page).to have_selector('ul input[checked]', count: 0)
+
+ find('.task-list-item-checkbox').click
+ wait_for_requests
+
+ visit_issue(project, issue)
+ wait_for_requests
+
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 1)
+ expect(page).to have_selector('ul input[checked]', count: 1)
+ expect(page).to have_content('1 of 1 checklist item completed')
end
end
@@ -370,6 +405,43 @@ RSpec.describe 'Task Lists', :js, feature_category: :team_planning do
end
end
+ describe 'summary properly formatted' do
+ let(:summary_markdown) do
+ <<-EOT.strip_heredoc
+ <details open>
+ <summary>Valid detail/summary with tasklist</summary>
+
+ - [ ] People Ops: do such and such
+
+ </details>
+
+ * [x] Task 1
+ EOT
+ end
+
+ let!(:issue) { create(:issue, description: summary_markdown, author: user, project: project) }
+
+ it 'renders' do
+ visit_issue(project, issue)
+ wait_for_requests
+
+ expect(page).to have_selector('ul.task-list', count: 2)
+ expect(page).to have_selector('li.task-list-item', count: 2)
+ expect(page).to have_selector('ul input[checked]', count: 1)
+
+ first('.task-list-item-checkbox').click
+ wait_for_requests
+
+ visit_issue(project, issue)
+ wait_for_requests
+
+ expect(page).to have_selector('ul.task-list', count: 2)
+ expect(page).to have_selector('li.task-list-item', count: 2)
+ expect(page).to have_selector('ul input[checked]', count: 2)
+ expect(page).to have_content('2 of 2 checklist items completed')
+ end
+ end
+
describe 'markdown starting with new line character' do
let(:markdown_starting_with_new_line) do
<<-EOT.strip_heredoc
diff --git a/spec/fixtures/api/schemas/entities/codequality_degradation.json b/spec/fixtures/api/schemas/entities/codequality_degradation.json
index 863b9f0c77e..ac772873daf 100644
--- a/spec/fixtures/api/schemas/entities/codequality_degradation.json
+++ b/spec/fixtures/api/schemas/entities/codequality_degradation.json
@@ -21,7 +21,10 @@
},
"web_url": {
"type": "string"
+ },
+ "engine_name": {
+ "type": "string"
}
},
"additionalProperties": false
-} \ No newline at end of file
+}
diff --git a/spec/frontend/fixtures/saved_replies.rb b/spec/frontend/fixtures/saved_replies.rb
new file mode 100644
index 00000000000..c80ba06bca1
--- /dev/null
+++ b/spec/frontend/fixtures/saved_replies.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GraphQL::Query, type: :request, feature_category: :user_profile do
+ include JavaScriptFixturesHelpers
+ include ApiHelpers
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+
+ before do
+ sign_in(current_user)
+ end
+
+ context 'when user has no saved replies' do
+ base_input_path = 'saved_replies/queries/'
+ base_output_path = 'graphql/saved_replies/'
+ query_name = 'saved_replies.query.graphql'
+
+ it "#{base_output_path}saved_replies_empty.query.graphql.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: current_user)
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'when user has saved replies' do
+ base_input_path = 'saved_replies/queries/'
+ base_output_path = 'graphql/saved_replies/'
+ query_name = 'saved_replies.query.graphql'
+
+ it "#{base_output_path}saved_replies.query.graphql.json" do
+ create(:saved_reply, user: current_user)
+ create(:saved_reply, user: current_user)
+
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: current_user)
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+end
diff --git a/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap b/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap
new file mode 100644
index 00000000000..3abdfcdaf20
--- /dev/null
+++ b/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Saved replies list item component renders list item 1`] = `
+<li
+ class="gl-mb-5"
+>
+ <div
+ class="gl-display-flex gl-align-items-center"
+ >
+ <strong>
+ test
+ </strong>
+ </div>
+
+ <div
+ class="gl-mt-3 gl-font-monospace"
+ >
+ /assign_reviewer
+ </div>
+</li>
+`;
diff --git a/spec/frontend/saved_replies/components/list_item_spec.js b/spec/frontend/saved_replies/components/list_item_spec.js
new file mode 100644
index 00000000000..cad1000473b
--- /dev/null
+++ b/spec/frontend/saved_replies/components/list_item_spec.js
@@ -0,0 +1,22 @@
+import { shallowMount } from '@vue/test-utils';
+import ListItem from '~/saved_replies/components/list_item.vue';
+
+let wrapper;
+
+function createComponent(propsData = {}) {
+ return shallowMount(ListItem, {
+ propsData,
+ });
+}
+
+describe('Saved replies list item component', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders list item', async () => {
+ wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer' } });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/saved_replies/components/list_spec.js b/spec/frontend/saved_replies/components/list_spec.js
new file mode 100644
index 00000000000..66e9ddfe148
--- /dev/null
+++ b/spec/frontend/saved_replies/components/list_spec.js
@@ -0,0 +1,68 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import noSavedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies_empty.query.graphql.json';
+import savedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies.query.graphql.json';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import List from '~/saved_replies/components/list.vue';
+import ListItem from '~/saved_replies/components/list_item.vue';
+import savedRepliesQuery from '~/saved_replies/queries/saved_replies.query.graphql';
+
+let wrapper;
+
+function createMockApolloProvider(response) {
+ Vue.use(VueApollo);
+
+ const requestHandlers = [[savedRepliesQuery, jest.fn().mockResolvedValue(response)]];
+
+ return createMockApollo(requestHandlers);
+}
+
+function createComponent(options = {}) {
+ const { mockApollo } = options;
+
+ return mount(List, {
+ apolloProvider: mockApollo,
+ });
+}
+
+describe('Saved replies list component', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('does not render any list items when response is empty', async () => {
+ const mockApollo = createMockApolloProvider(noSavedRepliesResponse);
+ wrapper = createComponent({ mockApollo });
+
+ await waitForPromises();
+
+ expect(wrapper.findAllComponents(ListItem).length).toBe(0);
+ });
+
+ it('render saved replies count', async () => {
+ const mockApollo = createMockApolloProvider(savedRepliesResponse);
+ wrapper = createComponent({ mockApollo });
+
+ await waitForPromises();
+
+ expect(wrapper.find('[data-testid="title"]').text()).toEqual('My saved replies (2)');
+ });
+
+ it('renders list of saved replies', async () => {
+ const mockApollo = createMockApolloProvider(savedRepliesResponse);
+ const savedReplies = savedRepliesResponse.data.currentUser.savedReplies.nodes;
+ wrapper = createComponent({ mockApollo });
+
+ await waitForPromises();
+
+ expect(wrapper.findAllComponents(ListItem).length).toBe(2);
+ expect(wrapper.findAllComponents(ListItem).at(0).props('reply')).toEqual(
+ expect.objectContaining(savedReplies[0]),
+ );
+ expect(wrapper.findAllComponents(ListItem).at(1).props('reply')).toEqual(
+ expect.objectContaining(savedReplies[1]),
+ );
+ });
+});
diff --git a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
index fe9b984f028..1e2ec7e8dc2 100644
--- a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
@@ -130,4 +130,11 @@ describe('Work Item Discussion', () => {
expect(findToggleRepliesWidget().props('collapsed')).toBe(false);
});
});
+
+ it('emits `deleteNote` event with correct parameter when child note component emits `deleteNote` event', () => {
+ createComponent();
+ findThreadAtIndex(0).vm.$emit('deleteNote');
+
+ expect(wrapper.emitted('deleteNote')).toEqual([[mockWorkItemCommentNote]]);
+ });
});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js
index d42d82c4e90..8f7d27def15 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js
@@ -1,4 +1,4 @@
-import { GlAvatarLink } from '@gitlab/ui';
+import { GlAvatarLink, GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
@@ -15,6 +15,8 @@ describe('Work Item Note', () => {
const findNoteHeader = () => wrapper.findComponent(NoteHeader);
const findNoteBody = () => wrapper.findComponent(NoteBody);
const findNoteActions = () => wrapper.findComponent(NoteActions);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-action"]');
const createComponent = ({ note = mockWorkItemCommentNote, isFirstNote = false } = {}) => {
wrapper = shallowMount(WorkItemNote, {
@@ -66,4 +68,34 @@ describe('Work Item Note', () => {
expect(findNoteActions().props('showReply')).toBe(false);
});
});
+
+ it('should display a dropdown if user has a permission to delete note', () => {
+ createComponent({
+ note: {
+ ...mockWorkItemCommentNote,
+ userPermissions: { ...mockWorkItemCommentNote.userPermissions, adminNote: true },
+ },
+ });
+
+ expect(findDropdown().exists()).toBe(true);
+ });
+
+ it('should not display a dropdown if user has no permission to delete note', () => {
+ createComponent();
+
+ expect(findDropdown().exists()).toBe(false);
+ });
+
+ it('should emit `deleteNote` event when delete note action is clicked', () => {
+ createComponent({
+ note: {
+ ...mockWorkItemCommentNote,
+ userPermissions: { ...mockWorkItemCommentNote.userPermissions, adminNote: true },
+ },
+ });
+
+ findDeleteNoteButton().vm.$emit('click');
+
+ expect(wrapper.emitted('deleteNote')).toEqual([[]]);
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_comment_form_spec.js b/spec/frontend/work_items/components/work_item_comment_form_spec.js
index e62eb32fad0..bef7efa2536 100644
--- a/spec/frontend/work_items/components/work_item_comment_form_spec.js
+++ b/spec/frontend/work_items/components/work_item_comment_form_spec.js
@@ -10,7 +10,7 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import WorkItemCommentForm from '~/work_items/components/work_item_comment_form.vue';
import WorkItemCommentLocked from '~/work_items/components/work_item_comment_locked.vue';
-import createNoteMutation from '~/work_items/graphql/create_work_item_note.mutation.graphql';
+import createNoteMutation from '~/work_items/graphql/notes/create_work_item_note.mutation.graphql';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js
index df9a141d330..e5b4bee68a8 100644
--- a/spec/frontend/work_items/components/work_item_notes_spec.js
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -1,16 +1,18 @@
-import { GlSkeletonLoader } from '@gitlab/ui';
+import { GlSkeletonLoader, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import SystemNote from '~/work_items/components/notes/system_note.vue';
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
import WorkItemCommentForm from '~/work_items/components/work_item_comment_form.vue';
import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
-import workItemNotesQuery from '~/work_items/graphql/work_item_notes.query.graphql';
-import workItemNotesByIidQuery from '~/work_items/graphql/work_item_notes_by_iid.query.graphql';
+import workItemNotesQuery from '~/work_items/graphql/notes/work_item_notes.query.graphql';
+import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
+import deleteWorkItemNoteMutation from '~/work_items/graphql/notes/delete_work_item_notes.mutation.graphql';
import { DEFAULT_PAGE_SIZE_NOTES, WIDGET_TYPE_NOTES } from '~/work_items/constants';
import { ASC, DESC } from '~/notes/constants';
import {
@@ -47,6 +49,8 @@ describe('WorkItemNotes component', () => {
Vue.use(VueApollo);
+ const showModal = jest.fn();
+
const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote);
const findAllListItems = () => wrapper.findAll('ul.timeline > *');
const findActivityLabel = () => wrapper.find('label');
@@ -56,6 +60,8 @@ describe('WorkItemNotes component', () => {
const findSystemNoteAtIndex = (index) => findAllSystemNotes().at(index);
const findAllWorkItemCommentNotes = () => wrapper.findAllComponents(WorkItemDiscussion);
const findWorkItemCommentNoteAtIndex = (index) => findAllWorkItemCommentNotes().at(index);
+ const findDeleteNoteModal = () => wrapper.findComponent(GlModal);
+
const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesResponse);
const workItemNotesByIidQueryHandler = jest
.fn()
@@ -64,16 +70,22 @@ describe('WorkItemNotes component', () => {
const workItemNotesWithCommentsQueryHandler = jest
.fn()
.mockResolvedValue(mockWorkItemNotesResponseWithComments);
+ const deleteWorkItemNoteMutationSuccessHandler = jest.fn().mockResolvedValue({
+ data: { destroyNote: { note: null, __typename: 'DestroyNote' } },
+ });
+ const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const createComponent = ({
workItemId = mockWorkItemId,
fetchByIid = false,
defaultWorkItemNotesQueryHandler = workItemNotesQueryHandler,
+ deleteWINoteMutationHandler = deleteWorkItemNoteMutationSuccessHandler,
} = {}) => {
wrapper = shallowMount(WorkItemNotes, {
apolloProvider: createMockApollo([
[workItemNotesQuery, defaultWorkItemNotesQueryHandler],
[workItemNotesByIidQuery, workItemNotesByIidQueryHandler],
+ [deleteWorkItemNoteMutation, deleteWINoteMutationHandler],
]),
propsData: {
workItemId,
@@ -89,6 +101,9 @@ describe('WorkItemNotes component', () => {
useIidInWorkItemsPath: fetchByIid,
},
},
+ stubs: {
+ GlModal: stubComponent(GlModal, { methods: { show: showModal } }),
+ },
});
};
@@ -240,4 +255,83 @@ describe('WorkItemNotes component', () => {
);
});
});
+
+ it('should open delete modal confirmation when child discussion emits `deleteNote` event', async () => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ });
+ await waitForPromises();
+
+ findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { id: '1', isLastNote: false });
+ expect(showModal).toHaveBeenCalled();
+ });
+
+ describe('when modal is open', () => {
+ beforeEach(() => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ });
+ return waitForPromises();
+ });
+
+ it('sends the mutation with correct variables', () => {
+ const noteId = 'some-test-id';
+
+ findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { id: noteId });
+ findDeleteNoteModal().vm.$emit('primary');
+
+ expect(deleteWorkItemNoteMutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: noteId,
+ },
+ });
+ });
+
+ it('successfully removes the note from the discussion', async () => {
+ expect(findWorkItemCommentNoteAtIndex(0).props('discussion')).toHaveLength(2);
+
+ findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', {
+ id: mockDiscussions[0].notes.nodes[0].id,
+ });
+ findDeleteNoteModal().vm.$emit('primary');
+
+ await waitForPromises();
+ expect(findWorkItemCommentNoteAtIndex(0).props('discussion')).toHaveLength(1);
+ });
+
+ it('successfully removes the discussion from work item if discussion only had one note', async () => {
+ const secondDiscussion = findWorkItemCommentNoteAtIndex(1);
+
+ expect(findAllWorkItemCommentNotes()).toHaveLength(2);
+ expect(secondDiscussion.props('discussion')).toHaveLength(1);
+
+ secondDiscussion.vm.$emit('deleteNote', {
+ id: mockDiscussions[1].notes.nodes[0].id,
+ discussion: { id: mockDiscussions[1].id },
+ });
+ findDeleteNoteModal().vm.$emit('primary');
+
+ await waitForPromises();
+ expect(findAllWorkItemCommentNotes()).toHaveLength(1);
+ });
+ });
+
+ it('emits `error` event if delete note mutation is rejected', async () => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ deleteWINoteMutationHandler: errorHandler,
+ });
+ await waitForPromises();
+
+ findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', {
+ id: mockDiscussions[0].notes.nodes[0].id,
+ });
+ findDeleteNoteModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong when deleting a comment. Please try again'],
+ ]);
+ });
});
diff --git a/spec/graphql/mutations/ci/job_token_scope/remove_project_spec.rb b/spec/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
index 7fb45e93474..a5294e96d71 100644
--- a/spec/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
+++ b/spec/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
@@ -66,7 +66,7 @@ RSpec.describe Mutations::Ci::JobTokenScope::RemoveProject, feature_category: :c
it 'executes project removal for the correct direction' do
expect(::Ci::JobTokenScope::RemoveProjectService)
.to receive(:new).with(project, current_user).and_return(service)
- expect(service).to receive(:execute).with(target_project, direction: 'inbound')
+ expect(service).to receive(:execute).with(target_project, 'inbound')
.and_return(instance_double('ServiceResponse', "success?": true))
subject
@@ -78,7 +78,7 @@ RSpec.describe Mutations::Ci::JobTokenScope::RemoveProject, feature_category: :c
it 'returns an error response' do
expect(::Ci::JobTokenScope::RemoveProjectService).to receive(:new).with(project, current_user).and_return(service)
- expect(service).to receive(:execute).with(target_project, direction: :outbound).and_return(ServiceResponse.error(message: 'The error message'))
+ expect(service).to receive(:execute).with(target_project, :outbound).and_return(ServiceResponse.error(message: 'The error message'))
expect(subject.fetch(:ci_job_token_scope)).to be_nil
expect(subject.fetch(:errors)).to include("The error message")
diff --git a/spec/lib/gitlab/database/schema_validation/database_spec.rb b/spec/lib/gitlab/database/schema_validation/database_spec.rb
new file mode 100644
index 00000000000..c0026f91b46
--- /dev/null
+++ b/spec/lib/gitlab/database/schema_validation/database_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::SchemaValidation::Database, feature_category: :database do
+ let(:database_name) { 'main' }
+ let(:database_indexes) do
+ [['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))']]
+ end
+
+ let(:query_result) { instance_double('ActiveRecord::Result', rows: database_indexes) }
+ let(:database_model) { Gitlab::Database.database_base_models[database_name] }
+ let(:connection) { database_model.connection }
+
+ subject(:database) { described_class.new(connection) }
+
+ before do
+ allow(connection).to receive(:exec_query).and_return(query_result)
+ end
+
+ describe '#fetch_index_by_name' do
+ context 'when index does not exist' do
+ it 'returns nil' do
+ index = database.fetch_index_by_name('non_existing_index')
+
+ expect(index).to be_nil
+ end
+ end
+
+ it 'returns index by name' do
+ index = database.fetch_index_by_name('index')
+
+ expect(index.name).to eq('index')
+ end
+ end
+
+ describe '#indexes' do
+ it 'returns indexes' do
+ indexes = database.indexes
+
+ expect(indexes).to all(be_a(Gitlab::Database::SchemaValidation::Index))
+ expect(indexes.map(&:name)).to eq(['index'])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/schema_validation/index_spec.rb b/spec/lib/gitlab/database/schema_validation/index_spec.rb
new file mode 100644
index 00000000000..297211d79ed
--- /dev/null
+++ b/spec/lib/gitlab/database/schema_validation/index_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::SchemaValidation::Index, feature_category: :database do
+ let(:index_statement) { 'CREATE INDEX index_name ON public.achievements USING btree (namespace_id)' }
+
+ let(:stmt) { PgQuery.parse(index_statement).tree.stmts.first.stmt.index_stmt }
+
+ let(:index) { described_class.new(stmt) }
+
+ describe '#name' do
+ it 'returns index name' do
+ expect(index.name).to eq('index_name')
+ end
+ end
+
+ describe '#statement' do
+ it 'returns index statement' do
+ expect(index.statement).to eq(index_statement)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/schema_validation/indexes_spec.rb b/spec/lib/gitlab/database/schema_validation/indexes_spec.rb
index 337597a49b0..4351031a4b4 100644
--- a/spec/lib/gitlab/database/schema_validation/indexes_spec.rb
+++ b/spec/lib/gitlab/database/schema_validation/indexes_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe Gitlab::Database::SchemaValidation::Indexes, feature_category: :d
let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') }
let(:database_indexes) do
[
- ['wrong_index', 'CREATE UNIQUE INDEX public.wrong_index ON table_name (column_name)'],
- ['extra_index', 'CREATE INDEX public.extra_index ON table_name (column_name)'],
+ ['wrong_index', 'CREATE UNIQUE INDEX wrong_index ON public.table_name (column_name)'],
+ ['extra_index', 'CREATE INDEX extra_index ON public.table_name (column_name)'],
['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))']
]
end
@@ -20,7 +20,10 @@ RSpec.describe Gitlab::Database::SchemaValidation::Indexes, feature_category: :d
let(:query_result) { instance_double('ActiveRecord::Result', rows: database_indexes) }
- subject(:schema_validation) { described_class.new(structure_file_path, database_name) }
+ let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) }
+ let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path) }
+
+ subject(:schema_validation) { described_class.new(structure_file, database) }
before do
allow(connection).to receive(:exec_query).and_return(query_result)
diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb
index 81751511270..fa0b3d1c6dd 100644
--- a/spec/lib/gitlab/etag_caching/middleware_spec.rb
+++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb
@@ -124,8 +124,8 @@ RSpec.describe Gitlab::EtagCaching::Middleware, :clean_gitlab_redis_shared_state
method: 'GET',
path: enabled_path,
status: status_code,
- request_urgency: :low,
- target_duration_s: 5,
+ request_urgency: :medium,
+ target_duration_s: 0.5,
metadata: a_hash_including(
{
'meta.caller_id' => 'Projects::NotesController#index',
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index ce67d1d0297..8a88328e0c1 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -5,7 +5,7 @@ require 'rspec-parameterized'
require 'support/helpers/rails_helpers'
RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cache, :clean_gitlab_redis_cache,
- feature_category: :scalability do
+ :use_null_store_as_repository_cache, feature_category: :scalability do
using RSpec::Parameterized::TableSyntax
describe '.add_instrumentation_data', :request_store do
@@ -23,42 +23,19 @@ RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cac
expect(payload).to include(db_count: 0, db_cached_count: 0, db_write_count: 0)
end
- shared_examples 'make Gitaly calls' do
- context 'when Gitaly calls are made' do
- it 'adds Gitaly and Redis data' do
- project = create(:project)
- RequestStore.clear!
- project.repository.exists?
+ context 'when Gitaly calls are made' do
+ it 'adds Gitaly and Redis data' do
+ project = create(:project)
+ RequestStore.clear!
+ project.repository.exists?
- subject
-
- expect(payload[:gitaly_calls]).to eq(1)
- expect(payload[:gitaly_duration_s]).to be >= 0
- # With MultiStore, the number of `redis_calls` depends on whether primary_store
- # (Gitlab::Redis::Repositorycache) and secondary_store (Gitlab::Redis::Cache) are of the same instance.
- # In GitLab.com CI, primary and secondary are the same instance, thus only 1 call being made. If primary
- # and secondary are different instances, an additional fallback read to secondary_store will be made because
- # the first `get` call is a cache miss. Then, the following expect will fail.
- expect(payload[:redis_calls]).to eq(1)
- expect(payload[:redis_duration_ms]).to be_nil
- end
- end
- end
-
- context 'when multistore ff use_primary_and_secondary_stores_for_repository_cache is enabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: true)
- end
-
- it_behaves_like 'make Gitaly calls'
- end
+ subject
- context 'when multistore ff use_primary_and_secondary_stores_for_repository_cache is disabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false)
+ expect(payload[:gitaly_calls]).to eq(1)
+ expect(payload[:gitaly_duration_s]).to be >= 0
+ expect(payload[:redis_calls]).to eq(nil)
+ expect(payload[:redis_duration_ms]).to be_nil
end
-
- it_behaves_like 'make Gitaly calls'
end
context 'when Redis calls are made' do
diff --git a/spec/lib/gitlab/redis/repository_cache_spec.rb b/spec/lib/gitlab/redis/repository_cache_spec.rb
index 56f77782778..8cdc4580f9e 100644
--- a/spec/lib/gitlab/redis/repository_cache_spec.rb
+++ b/spec/lib/gitlab/redis/repository_cache_spec.rb
@@ -4,43 +4,6 @@ require 'spec_helper'
RSpec.describe Gitlab::Redis::RepositoryCache, feature_category: :scalability do
include_examples "redis_new_instance_shared_examples", 'repository_cache', Gitlab::Redis::Cache
- include_examples "redis_shared_examples"
-
- describe '#pool' do
- let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
- let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
-
- subject { described_class.pool }
-
- before do
- allow(described_class).to receive(:config_file_name).and_return(config_new_format_host)
-
- # Override rails root to avoid having our fixtures overwritten by `redis.yml` if it exists
- allow(Gitlab::Redis::Cache).to receive(:rails_root).and_return(mktmpdir)
- allow(Gitlab::Redis::Cache).to receive(:config_file_name).and_return(config_new_format_socket)
- end
-
- around do |example|
- clear_pool
- example.run
- ensure
- clear_pool
- end
-
- it 'instantiates an instance of MultiStore' do
- subject.with do |redis_instance|
- expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore)
-
- expect(redis_instance.primary_store.connection[:id]).to eq("redis://test-host:6379/99")
- expect(redis_instance.secondary_store.connection[:id]).to eq("unix:///path/to/redis.sock/0")
-
- expect(redis_instance.instance_name).to eq('RepositoryCache')
- end
- end
-
- it_behaves_like 'multi store feature flags', :use_primary_and_secondary_stores_for_repository_cache,
- :use_primary_store_as_default_for_repository_cache
- end
describe '#raw_config_hash' do
it 'has a legacy default URL' do
@@ -49,4 +12,10 @@ RSpec.describe Gitlab::Redis::RepositoryCache, feature_category: :scalability do
expect(subject.send(:raw_config_hash)).to eq(url: 'redis://localhost:6380')
end
end
+
+ describe '.cache_store' do
+ it 'has a default ttl of 8 hours' do
+ expect(described_class.cache_store.options[:expires_in]).to eq(8.hours)
+ end
+ end
end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index ce5d0cfa632..406de3403f3 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -1089,4 +1089,73 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
it { is_expected.not_to match('random string') }
it { is_expected.not_to match('12321342545356434523412341245452345623453542345234523453245') }
end
+
+ describe 'code, html blocks, or html comment blocks regex' do
+ context 'code blocks' do
+ subject { described_class::MARKDOWN_CODE_BLOCK_REGEX }
+
+ let(:expected) { %(```code\nsome code\n\n>>>\nthat includes a multiline-blockquote\n>>>\n```) }
+ let(:markdown) do
+ <<~MARKDOWN
+ Regular text
+
+ ```code
+ some code
+
+ >>>
+ that includes a multiline-blockquote
+ >>>
+ ```
+ MARKDOWN
+ end
+
+ it { is_expected.to match(%(```ruby\nsomething\n```)) }
+ it { is_expected.not_to match(%(must start in first column ```ruby\nsomething\n```)) }
+ it { is_expected.not_to match(%(```ruby must be multi-line ```)) }
+ it { expect(subject.match(markdown)[:code]).to eq expected }
+ end
+
+ context 'HTML blocks' do
+ subject { described_class::MARKDOWN_HTML_BLOCK_REGEX }
+
+ let(:expected) { %(<section>\n<p>paragraph</p>\n\n>>>\nthat includes a multiline-blockquote\n>>>\n</section>) }
+ let(:markdown) do
+ <<~MARKDOWN
+ Regular text
+
+ <section>
+ <p>paragraph</p>
+
+ >>>
+ that includes a multiline-blockquote
+ >>>
+ </section>
+ MARKDOWN
+ end
+
+ it { is_expected.to match(%(<section>\nsomething\n</section>)) }
+ it { is_expected.not_to match(%(must start in first column <section>\nsomething\n</section>)) }
+ it { is_expected.not_to match(%(<section>must be multi-line</section>)) }
+ it { expect(subject.match(markdown)[:html]).to eq expected }
+ end
+
+ context 'HTML comment blocks' do
+ subject { described_class::MARKDOWN_HTML_COMMENT_BLOCK_REGEX }
+
+ let(:expected) { %(<!-- the start of an HTML comment\n- [ ] list item commented out\n-->) }
+ let(:markdown) do
+ <<~MARKDOWN
+ Regular text
+
+ <!-- the start of an HTML comment
+ - [ ] list item commented out
+ -->
+ MARKDOWN
+ end
+
+ it { is_expected.to match(%(<!--\ncomment\n-->)) }
+ it { is_expected.not_to match(%(must start in first column <!--\ncomment\n-->)) }
+ it { expect(subject.match(markdown)[:html_block_comment]).to eq expected }
+ end
+ end
end
diff --git a/spec/lib/gitlab/repository_cache/preloader_spec.rb b/spec/lib/gitlab/repository_cache/preloader_spec.rb
index 21628481fed..e6fb0da6412 100644
--- a/spec/lib/gitlab/repository_cache/preloader_spec.rb
+++ b/spec/lib/gitlab/repository_cache/preloader_spec.rb
@@ -6,76 +6,51 @@ RSpec.describe Gitlab::RepositoryCache::Preloader, :use_clean_rails_redis_cachin
feature_category: :source_code_management do
let(:projects) { create_list(:project, 2, :repository) }
let(:repositories) { projects.map(&:repository) }
+ let(:cache) { Gitlab::RepositoryCache.store }
- before do
- stub_feature_flags(use_primary_store_as_default_for_repository_cache: false)
- end
-
- shared_examples 'preload' do
- describe '#preload' do
- context 'when the values are already cached' do
- before do
- # Warm the cache but use a different model so they are not memoized
- repos = Project.id_in(projects).order(:id).map(&:repository)
-
- allow(repos[0].head_tree).to receive(:readme_path).and_return('README.txt')
- allow(repos[1].head_tree).to receive(:readme_path).and_return('README.md')
-
- repos.map(&:exists?)
- repos.map(&:readme_path)
- end
-
- it 'prevents individual cache reads for cached methods' do
- expect(cache).to receive(:read_multi).once.and_call_original
-
- described_class.new(repositories).preload(
- %i[exists? readme_path]
- )
-
- expect(cache).not_to receive(:read)
- expect(cache).not_to receive(:write)
+ describe '#preload' do
+ context 'when the values are already cached' do
+ before do
+ # Warm the cache but use a different model so they are not memoized
+ repos = Project.id_in(projects).order(:id).map(&:repository)
- expect(repositories[0].exists?).to eq(true)
- expect(repositories[0].readme_path).to eq('README.txt')
+ allow(repos[0].head_tree).to receive(:readme_path).and_return('README.txt')
+ allow(repos[1].head_tree).to receive(:readme_path).and_return('README.md')
- expect(repositories[1].exists?).to eq(true)
- expect(repositories[1].readme_path).to eq('README.md')
- end
+ repos.map(&:exists?)
+ repos.map(&:readme_path)
end
- context 'when values are not cached' do
- it 'reads and writes from cache individually' do
- described_class.new(repositories).preload(
- %i[exists? has_visible_content?]
- )
+ it 'prevents individual cache reads for cached methods' do
+ expect(cache).to receive(:read_multi).once.and_call_original
- expect(cache).to receive(:read).exactly(4).times
- expect(cache).to receive(:write).exactly(4).times
+ described_class.new(repositories).preload(
+ %i[exists? readme_path]
+ )
- repositories.each(&:exists?)
- repositories.each(&:has_visible_content?)
- end
- end
- end
- end
+ expect(cache).not_to receive(:read)
+ expect(cache).not_to receive(:write)
- context 'when use_primary_and_secondary_stores_for_repository_cache feature flag is enabled' do
- let(:cache) { Gitlab::RepositoryCache.store }
+ expect(repositories[0].exists?).to eq(true)
+ expect(repositories[0].readme_path).to eq('README.txt')
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: true)
+ expect(repositories[1].exists?).to eq(true)
+ expect(repositories[1].readme_path).to eq('README.md')
+ end
end
- it_behaves_like 'preload'
- end
+ context 'when values are not cached' do
+ it 'reads and writes from cache individually' do
+ described_class.new(repositories).preload(
+ %i[exists? has_visible_content?]
+ )
- context 'when use_primary_and_secondary_stores_for_repository_cache feature flag is disabled' do
- let(:cache) { Rails.cache }
+ expect(cache).to receive(:read).exactly(4).times
+ expect(cache).to receive(:write).exactly(4).times
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false)
+ repositories.each(&:exists?)
+ repositories.each(&:has_visible_content?)
+ end
end
-
- it_behaves_like 'preload'
end
end
diff --git a/spec/lib/gitlab/repository_hash_cache_spec.rb b/spec/lib/gitlab/repository_hash_cache_spec.rb
index d41bf45f72e..6b52c315a70 100644
--- a/spec/lib/gitlab/repository_hash_cache_spec.rb
+++ b/spec/lib/gitlab/repository_hash_cache_spec.rb
@@ -69,35 +69,20 @@ RSpec.describe Gitlab::RepositoryHashCache, :clean_gitlab_redis_cache do
end
end
- shared_examples "key?" do
- describe "#key?" do
- subject { cache.key?(:example, "test") }
+ describe "#key?" do
+ subject { cache.key?(:example, "test") }
- context "key exists" do
- before do
- cache.write(:example, test_hash)
- end
-
- it { is_expected.to be(true) }
+ context "key exists" do
+ before do
+ cache.write(:example, test_hash)
end
- context "key doesn't exist" do
- it { is_expected.to be(false) }
- end
+ it { is_expected.to be(true) }
end
- end
-
- context "when both multistore FF is enabled" do
- it_behaves_like "key?"
- end
- context "when both multistore FF is disabled" do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false)
- stub_feature_flags(use_primary_store_as_default_for_repository_cache: false)
+ context "key doesn't exist" do
+ it { is_expected.to be(false) }
end
-
- it_behaves_like "key?"
end
describe "#read_members" do
diff --git a/spec/models/concerns/taskable_spec.rb b/spec/models/concerns/taskable_spec.rb
index 140f6cda51c..0ad29454ff3 100644
--- a/spec/models/concerns/taskable_spec.rb
+++ b/spec/models/concerns/taskable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Taskable do
+RSpec.describe Taskable, feature_category: :team_planning do
using RSpec::Parameterized::TableSyntax
describe '.get_tasks' do
@@ -13,8 +13,18 @@ RSpec.describe Taskable do
- [x] Second item
* [x] First item
* [ ] Second item
+
+ <!-- a comment
+ - [ ] Item in comment, ignore
+ rest of comment -->
+
+ [ ] No-break space (U+00A0)
+ [ ] Figure space (U+2007)
+
+ ```
+ - [ ] Item in code, ignore
+ ```
+
+ [ ] Narrow no-break space (U+202F)
+ [ ] Thin space (U+2009)
MARKDOWN
diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
index 805f300072d..f1296c054f9 100644
--- a/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
@@ -76,6 +76,15 @@ RSpec.describe 'CiJobTokenScopeRemoveProject', feature_category: :continuous_int
end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(-1)
end
+ it 'responds successfully' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(graphql_errors).to be_nil
+ expect(graphql_data_at(:ciJobTokenScopeRemoveProject, :ciJobTokenScope, :projects, :nodes))
+ .to contain_exactly({ 'path' => project.path })
+ end
+
context 'when invalid target project is provided' do
before do
variables[:target_project_path] = 'unknown/project'
diff --git a/spec/requests/profiles/saved_replies_controller_spec.rb b/spec/requests/profiles/saved_replies_controller_spec.rb
new file mode 100644
index 00000000000..27a961a201f
--- /dev/null
+++ b/spec/requests/profiles/saved_replies_controller_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Profiles::SavedRepliesController, feature_category: :user_profile do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET #index' do
+ describe 'feature flag disabled' do
+ before do
+ stub_feature_flags(saved_replies: false)
+
+ get '/-/profile/saved_replies'
+ end
+
+ it { expect(response).to have_gitlab_http_status(:not_found) }
+ end
+
+ describe 'feature flag enabled' do
+ before do
+ get '/-/profile/saved_replies'
+ end
+
+ it { expect(response).to have_gitlab_http_status(:ok) }
+
+ it 'sets hide search settings ivar' do
+ expect(assigns(:hide_search_settings)).to eq(true)
+ end
+ end
+ end
+end
diff --git a/spec/requests/projects/noteable_notes_spec.rb b/spec/requests/projects/noteable_notes_spec.rb
index 084cf2d4a5c..55540447da0 100644
--- a/spec/requests/projects/noteable_notes_spec.rb
+++ b/spec/requests/projects/noteable_notes_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe 'Project noteable notes', feature_category: :team_planning do
expect(Gitlab::Metrics::RailsSlis.request_apdex).to(
receive(:increment).with(
labels: {
- request_urgency: :low,
+ request_urgency: :medium,
feature_category: "team_planning",
endpoint_id: "Projects::NotesController#index"
},
@@ -57,8 +57,8 @@ RSpec.describe 'Project noteable notes', feature_category: :team_planning do
'process_action.action_controller',
a_hash_including(
{
- request_urgency: :low,
- target_duration_s: 5,
+ request_urgency: :medium,
+ target_duration_s: 0.5,
metadata: a_hash_including({
'meta.feature_category' => 'team_planning',
'meta.caller_id' => "Projects::NotesController#index"
diff --git a/spec/serializers/codequality_degradation_entity_spec.rb b/spec/serializers/codequality_degradation_entity_spec.rb
index 0390e232fd5..32269e5475b 100644
--- a/spec/serializers/codequality_degradation_entity_spec.rb
+++ b/spec/serializers/codequality_degradation_entity_spec.rb
@@ -18,6 +18,7 @@ RSpec.describe CodequalityDegradationEntity do
expect(subject[:file_path]).to eq("file_a.rb")
expect(subject[:line]).to eq(10)
expect(subject[:web_url]).to eq("http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_a.rb#L10")
+ expect(subject[:engine_name]).to eq('structure')
end
end
@@ -30,6 +31,7 @@ RSpec.describe CodequalityDegradationEntity do
expect(subject[:file_path]).to eq("file_b.rb")
expect(subject[:line]).to eq(10)
expect(subject[:web_url]).to eq("http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_b.rb#L10")
+ expect(subject[:engine_name]).to eq('rubocop')
end
end
@@ -46,6 +48,7 @@ RSpec.describe CodequalityDegradationEntity do
expect(subject[:file_path]).to eq("file_b.rb")
expect(subject[:line]).to eq(10)
expect(subject[:web_url]).to eq("http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_b.rb#L10")
+ expect(subject[:engine_name]).to eq('rubocop')
end
end
end
diff --git a/spec/services/ci/job_token_scope/remove_project_service_spec.rb b/spec/services/ci/job_token_scope/remove_project_service_spec.rb
index e154d8c0422..5b39f8908f2 100644
--- a/spec/services/ci/job_token_scope/remove_project_service_spec.rb
+++ b/spec/services/ci/job_token_scope/remove_project_service_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Ci::JobTokenScope::RemoveProjectService, feature_category: :conti
end
describe '#execute' do
- subject(:result) { service.execute(target_project) }
+ subject(:result) { service.execute(target_project, :outbound) }
it_behaves_like 'editable job token scope' do
context 'when user has permissions on source and target project' do
diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb
index 2c1ebe27014..be059aec697 100644
--- a/spec/services/projects/import_export/export_service_spec.rb
+++ b/spec/services/projects/import_export/export_service_spec.rb
@@ -2,11 +2,12 @@
require 'spec_helper'
-RSpec.describe Projects::ImportExport::ExportService do
+RSpec.describe Projects::ImportExport::ExportService, feature_category: :importers do
describe '#execute' do
let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be_with_reload(:project) { create(:project, group: group) }
- let(:project) { create(:project) }
let(:shared) { project.import_export_shared }
let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new }
@@ -220,5 +221,21 @@ RSpec.describe Projects::ImportExport::ExportService do
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error).with_message(expected_message)
end
end
+
+ it "avoids N+1 when exporting project members" do
+ group.add_owner(user)
+ group.add_maintainer(create(:user))
+ project.add_maintainer(create(:user))
+
+ # warm up
+ service.execute
+
+ control = ActiveRecord::QueryRecorder.new { service.execute }
+
+ group.add_maintainer(create(:user))
+ project.add_maintainer(create(:user))
+
+ expect { service.execute }.not_to exceed_query_limit(control)
+ end
end
end
diff --git a/spec/support/redis.rb b/spec/support/redis.rb
index 6d313c8aa16..d5ae0bf1582 100644
--- a/spec/support/redis.rb
+++ b/spec/support/redis.rb
@@ -25,4 +25,10 @@ RSpec.configure do |config|
instance_class.with(&:flushdb)
end
end
+
+ config.before(:each, :use_null_store_as_repository_cache) do |example|
+ null_store = ActiveSupport::Cache::NullStore.new
+
+ allow(Gitlab::Redis::RepositoryCache).to receive(:cache_store).and_return(null_store)
+ end
end
diff --git a/spec/tasks/cache/clear/redis_spec.rb b/spec/tasks/cache/clear/redis_spec.rb
index 9b6ea3891d9..375d01bf2ba 100644
--- a/spec/tasks/cache/clear/redis_spec.rb
+++ b/spec/tasks/cache/clear/redis_spec.rb
@@ -3,7 +3,7 @@
require 'rake_helper'
RSpec.describe 'clearing redis cache', :clean_gitlab_redis_repository_cache, :clean_gitlab_redis_cache,
- :silence_stdout, feature_category: :redis do
+ :silence_stdout, :use_null_store_as_repository_cache, feature_category: :redis do
before do
Rake.application.rake_require 'tasks/cache'
end
@@ -20,37 +20,11 @@ RSpec.describe 'clearing redis cache', :clean_gitlab_redis_repository_cache, :cl
create(:ci_pipeline, project: project).project.pipeline_status
end
- context 'when use_primary_and_secondary_stores_for_repository_cache MultiStore FF is enabled' do
- # Initially, project:{id}:pipeline_status is explicitly cached in Gitlab::Redis::Cache, whereas repository is
- # cached in Rails.cache (which is a NullStore).
- # With the MultiStore feature flag enabled, we use Gitlab::Redis::RepositoryCache instance as primary store and
- # Gitlab::Redis::Cache as secondary store.
- # This ends up storing 2 extra keys (exists? and root_ref) in both Gitlab::Redis::RepositoryCache and
- # Gitlab::Redis::Cache instances when loading project.pipeline_status
- let(:keys_size_changed) { -3 }
-
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: true)
- allow(pipeline_status).to receive(:loaded).and_return(nil)
- end
-
- it 'clears pipeline status cache' do
- expect { run_rake_task('cache:clear:redis') }.to change { pipeline_status.has_cache? }
- end
-
- it_behaves_like 'clears the cache'
+ before do
+ allow(pipeline_status).to receive(:loaded).and_return(nil)
end
- context 'when use_primary_and_secondary_stores_for_repository_cache and
- use_primary_store_as_default_for_repository_cache feature flags are disabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false)
- stub_feature_flags(use_primary_store_as_default_for_repository_cache: false)
- allow(pipeline_status).to receive(:loaded).and_return(nil)
- end
-
- it_behaves_like 'clears the cache'
- end
+ it_behaves_like 'clears the cache'
end
describe 'clearing set caches' do
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index c583631e4d5..c0d46a206ce 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -364,6 +364,7 @@ RSpec.describe 'Every Sidekiq worker' do
'Onboarding::PipelineCreatedWorker' => 3,
'Onboarding::ProgressWorker' => 3,
'Onboarding::UserAddedWorker' => 3,
+ 'Namespaces::FreeUserCap::OverLimitNotificationWorker' => false,
'Namespaces::RefreshRootStatisticsWorker' => 3,
'Namespaces::RootStatisticsWorker' => 3,
'Namespaces::ScheduleAggregationWorker' => 3,