Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-19 18:08:55 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-19 18:08:55 +0300
commit51238b2afe1ef4783b41368fa1e22b50f473c070 (patch)
tree8efb6e48b1edf709f051d03c6c5767f2d46730ab
parentba55ca9bc4bf2c85d2d78fcb11552ad130151110 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum13
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/javascripts/profile/components/graphql/get_user_snippets.query.graphql39
-rw-r--r--app/assets/javascripts/profile/components/profile_tabs.vue2
-rw-r--r--app/assets/javascripts/profile/components/snippets/snippet_row.vue21
-rw-r--r--app/assets/javascripts/profile/components/snippets/snippets_tab.vue110
-rw-r--r--app/assets/javascripts/profile/components/snippets_tab.vue17
-rw-r--r--app/assets/javascripts/profile/constants.js2
-rw-r--r--app/assets/javascripts/profile/index.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue16
-rw-r--r--app/helpers/users_helper.rb3
-rw-r--r--app/services/concerns/search/filter.rb13
-rw-r--r--app/services/search/global_service.rb3
-rw-r--r--app/services/search/group_service.rb2
-rw-r--r--app/services/search/project_service.rb4
-rw-r--r--config/feature_flags/development/generate_commit_message_flag.yml8
-rw-r--r--config/feature_flags/development/generate_commit_message_vertex.yml8
-rw-r--r--config/initializers/1_settings.rb6
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--doc/api/graphql/reference/index.md11
-rw-r--r--doc/architecture/blueprints/ci_pipeline_components/img/catalogs.pngbin0 -> 102551 bytes
-rw-r--r--doc/architecture/blueprints/ci_pipeline_components/index.md23
-rw-r--r--doc/user/product_analytics/index.md52
-rw-r--r--lib/gitlab/avatar_cache.rb10
-rw-r--r--lib/gitlab/error_tracking/processor/grpc_error_processor.rb3
-rw-r--r--lib/gitlab/reactive_cache_set_cache.rb14
-rw-r--r--lib/gitlab/set_cache.rb10
-rw-r--r--lib/gitlab/usage_data_counters/known_events/workspaces.yml5
-rw-r--r--locale/gitlab.pot15
-rw-r--r--package.json2
-rw-r--r--qa/Gemfile.lock4
-rw-r--r--qa/qa/tools/migrate_influx_data_to_gcs.rb112
-rw-r--r--qa/tasks/migrate_influx_data_to_gcs.rake6
-rw-r--r--spec/frontend/profile/components/profile_tabs_spec.js2
-rw-r--r--spec/frontend/profile/components/snippets/snippet_row_spec.js31
-rw-r--r--spec/frontend/profile/components/snippets/snippets_tab_spec.js162
-rw-r--r--spec/frontend/profile/components/snippets_tab_spec.js19
-rw-r--r--spec/frontend/profile/mock_data.js76
-rw-r--r--spec/helpers/users_helper_spec.rb5
-rw-r--r--spec/lib/gitlab/avatar_cache_spec.rb10
-rw-r--r--spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb2
-rw-r--r--spec/lib/gitlab/reactive_cache_set_cache_spec.rb14
-rw-r--r--spec/lib/gitlab/repository_set_cache_spec.rb10
-rw-r--r--spec/support/helpers/database/multiple_databases_helpers.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/search_labels_filter_shared_examples.rb19
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb2
-rw-r--r--workhorse/go.mod2
-rw-r--r--workhorse/go.sum4
-rw-r--r--yarn.lock8
50 files changed, 844 insertions, 85 deletions
diff --git a/Gemfile b/Gemfile
index b4ea7288482..eec27dddbc6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -510,7 +510,7 @@ gem 'gitaly', '~> 15.9.0-rc3'
# KAS GRPC protocol definitions
gem 'kas-grpc', '~> 0.1.0'
-gem 'grpc', '~> 1.54.2'
+gem 'grpc', '~> 1.42.0'
gem 'google-protobuf', '~> 3.23', '>= 3.23.1'
diff --git a/Gemfile.checksum b/Gemfile.checksum
index f507322ace0..bac1e6990fd 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -269,13 +269,12 @@
{"name":"graphql","version":"1.13.12","platform":"ruby","checksum":"1d82666cf201193a8d0cb54cea38576b820418db4869b549f61a35f3a2d97ac3"},
{"name":"graphql-client","version":"0.17.0","platform":"ruby","checksum":"5aaf02ce8f2dbc8e3ba05a7eaeb3ad9336762c4424c6093f4438fbb9490eeb5d"},
{"name":"graphql-docs","version":"2.1.0","platform":"ruby","checksum":"7eb82402f8fda455104b2b60364e9ada145d79d3121a8f915790d49da38bb576"},
-{"name":"grpc","version":"1.54.2","platform":"ruby","checksum":"036f931bcf8d43e1b65eb1c1dc010e1ab24fb64ed7e8703948a8288eda61812f"},
-{"name":"grpc","version":"1.54.2","platform":"x64-mingw-ucrt","checksum":"adbfa4b6004d229ecd75d8f33e07be8dcdf185b30263724288a75154dc9a3637"},
-{"name":"grpc","version":"1.54.2","platform":"x64-mingw32","checksum":"5c1c1ff87bdf928c442ec4c7166fb8abd4fcce00d7ec6e60313f488919f5d4f5"},
-{"name":"grpc","version":"1.54.2","platform":"x86-linux","checksum":"e8cf13e91714f33bba0ad06a3f4b9d98f056d09d7300e92ccb499cc95cc5cb9d"},
-{"name":"grpc","version":"1.54.2","platform":"x86-mingw32","checksum":"7e28b9a2e60d1fea6a5a774e20978628b27220ac9fe0a2a9c9e9cf2874700de4"},
-{"name":"grpc","version":"1.54.2","platform":"x86_64-darwin","checksum":"57b9effbdc25c43ebd706c4e9575baf31fd975a52cde67d41d8d33a7264e508e"},
-{"name":"grpc","version":"1.54.2","platform":"x86_64-linux","checksum":"d8ca04d8741b7d646c0634292f68925e95e29cad9805bcf19e820dd67d3e4bbb"},
+{"name":"grpc","version":"1.42.0","platform":"ruby","checksum":"b3d2649e67c6a636544996843d9ec191699c54c1aca797dbfea4dff36c14584a"},
+{"name":"grpc","version":"1.42.0","platform":"x64-mingw32","checksum":"6aac1b6576134b0a83e000b1269f60d502eb24aee96c64e2658c3f24f8e32ac0"},
+{"name":"grpc","version":"1.42.0","platform":"x86-linux","checksum":"4aa50538aa929f1f3bcefb11c65ee1a1606b5aef838ea4d4e93c100b5f4263a5"},
+{"name":"grpc","version":"1.42.0","platform":"x86-mingw32","checksum":"eeb2a9381bea43fafe879b6ddaa011351a44d0894d48bdc965a07bcb67c6eb56"},
+{"name":"grpc","version":"1.42.0","platform":"x86_64-darwin","checksum":"20fa202d46d8a055628260622e98fb6439529fbac283f0552af620b909f78535"},
+{"name":"grpc","version":"1.42.0","platform":"x86_64-linux","checksum":"92e2ceb2aca335d5755163dd8030082091d5b0e63c117b1ca07051b66c53eb2e"},
{"name":"gssapi","version":"1.3.1","platform":"ruby","checksum":"c51cf30842ee39bd93ce7fc33e20405ff8a04cda9dec6092071b61258284aee1"},
{"name":"guard","version":"2.16.2","platform":"ruby","checksum":"71ba7abaddecc8be91ab77bbaf78f767246603652ebbc7b976fda497ebdc8fbb"},
{"name":"guard-compat","version":"1.2.1","platform":"ruby","checksum":"3ad21ab0070107f92edfd82610b5cdc2fb8e368851e72362ada9703443d646fe"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 1c807080efc..c804e096235 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -750,8 +750,8 @@ GEM
graphql (~> 1.12)
html-pipeline (~> 2.9)
sass (~> 3.4)
- grpc (1.54.2)
- google-protobuf (~> 3.21)
+ grpc (1.42.0)
+ google-protobuf (~> 3.18)
googleapis-common-protos-types (~> 1.0)
gssapi (1.3.1)
ffi (>= 1.0.1)
@@ -1777,7 +1777,7 @@ DEPENDENCIES
graphlyte (~> 1.0.0)
graphql (~> 1.13.12)
graphql-docs (~> 2.1.0)
- grpc (~> 1.54.2)
+ grpc (~> 1.42.0)
gssapi (~> 1.3.1)
guard-rspec
haml_lint (~> 0.40.0)
diff --git a/app/assets/javascripts/profile/components/graphql/get_user_snippets.query.graphql b/app/assets/javascripts/profile/components/graphql/get_user_snippets.query.graphql
new file mode 100644
index 00000000000..6a16557fb8b
--- /dev/null
+++ b/app/assets/javascripts/profile/components/graphql/get_user_snippets.query.graphql
@@ -0,0 +1,39 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+
+query getUserSnippets(
+ $id: UserID!
+ $first: Int
+ $last: Int
+ $afterToken: String
+ $beforeToken: String
+) {
+ user(id: $id) {
+ id
+ avatarUrl
+ name
+ username
+ snippets(first: $first, last: $last, before: $beforeToken, after: $afterToken) {
+ pageInfo {
+ ...PageInfo
+ }
+ nodes {
+ id
+ title
+ webUrl
+ visibilityLevel
+ createdAt
+ updatedAt
+ blobs {
+ nodes {
+ name
+ }
+ }
+ commenters {
+ nodes {
+ id
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/profile/components/profile_tabs.vue b/app/assets/javascripts/profile/components/profile_tabs.vue
index 8e52a98803d..fdc31edfe5f 100644
--- a/app/assets/javascripts/profile/components/profile_tabs.vue
+++ b/app/assets/javascripts/profile/components/profile_tabs.vue
@@ -11,7 +11,7 @@ import GroupsTab from './groups_tab.vue';
import ContributedProjectsTab from './contributed_projects_tab.vue';
import PersonalProjectsTab from './personal_projects_tab.vue';
import StarredProjectsTab from './starred_projects_tab.vue';
-import SnippetsTab from './snippets_tab.vue';
+import SnippetsTab from './snippets/snippets_tab.vue';
import FollowersTab from './followers_tab.vue';
import FollowingTab from './following_tab.vue';
diff --git a/app/assets/javascripts/profile/components/snippets/snippet_row.vue b/app/assets/javascripts/profile/components/snippets/snippet_row.vue
new file mode 100644
index 00000000000..b2eed2a9189
--- /dev/null
+++ b/app/assets/javascripts/profile/components/snippets/snippet_row.vue
@@ -0,0 +1,21 @@
+<script>
+export default {
+ name: 'SnippetRow',
+ props: {
+ snippet: {
+ type: Object,
+ required: true,
+ },
+ userInfo: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ {{ snippet.title }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/profile/components/snippets/snippets_tab.vue b/app/assets/javascripts/profile/components/snippets/snippets_tab.vue
new file mode 100644
index 00000000000..fce5e2f5e78
--- /dev/null
+++ b/app/assets/javascripts/profile/components/snippets/snippets_tab.vue
@@ -0,0 +1,110 @@
+<script>
+import { GlTab, GlKeysetPagination, GlEmptyState } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPENAME_USER } from '~/graphql_shared/constants';
+import { SNIPPET_MAX_LIST_COUNT } from '~/profile/constants';
+import getUserSnippets from '../graphql/get_user_snippets.query.graphql';
+import SnippetRow from './snippet_row.vue';
+
+export default {
+ name: 'SnippetsTab',
+ i18n: {
+ title: s__('UserProfile|Snippets'),
+ noSnippets: s__('UserProfiles|No snippets found.'),
+ },
+ components: {
+ GlTab,
+ GlKeysetPagination,
+ GlEmptyState,
+ SnippetRow,
+ },
+ inject: ['userId', 'snippetsEmptyState'],
+ data() {
+ return {
+ userInfo: {},
+ pageInfo: {},
+ cursor: {
+ first: SNIPPET_MAX_LIST_COUNT,
+ last: null,
+ },
+ };
+ },
+ apollo: {
+ userSnippets: {
+ query: getUserSnippets,
+ variables() {
+ return {
+ id: convertToGraphQLId(TYPENAME_USER, this.userId),
+ ...this.cursor,
+ };
+ },
+ update(data) {
+ this.userInfo = {
+ avatarUrl: data.user?.avatarUrl,
+ name: data.user?.name,
+ username: data.user?.username,
+ };
+ this.pageInfo = data?.user?.snippets?.pageInfo;
+ return data?.user?.snippets?.nodes || [];
+ },
+ error() {
+ return [];
+ },
+ },
+ },
+ computed: {
+ hasSnippets() {
+ return this.userSnippets?.length;
+ },
+ },
+ methods: {
+ isLastSnippet(index) {
+ return index === this.userSnippets.length - 1;
+ },
+ nextPage() {
+ this.cursor = {
+ first: SNIPPET_MAX_LIST_COUNT,
+ last: null,
+ afterToken: this.pageInfo.endCursor,
+ };
+ },
+ prevPage() {
+ this.cursor = {
+ first: null,
+ last: SNIPPET_MAX_LIST_COUNT,
+ beforeToken: this.pageInfo.startCursor,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-tab :title="$options.i18n.title">
+ <template v-if="hasSnippets">
+ <snippet-row
+ v-for="(snippet, index) in userSnippets"
+ :key="snippet.id"
+ :snippet="snippet"
+ :user-info="userInfo"
+ :class="{ 'gl-border-b': !isLastSnippet(index) }"
+ />
+ <div class="gl-display-flex gl-justify-content-center gl-mt-6">
+ <gl-keyset-pagination
+ v-if="pageInfo.hasPreviousPage || pageInfo.hasNextPage"
+ v-bind="pageInfo"
+ @prev="prevPage"
+ @next="nextPage"
+ />
+ </div>
+ </template>
+ <template v-if="!hasSnippets">
+ <gl-empty-state class="gl-mt-5" :svg-height="75" :svg-path="snippetsEmptyState">
+ <template #title>
+ <p class="gl-font-weight-bold gl-mt-n5">{{ $options.i18n.noSnippets }}</p>
+ </template>
+ </gl-empty-state>
+ </template>
+ </gl-tab>
+</template>
diff --git a/app/assets/javascripts/profile/components/snippets_tab.vue b/app/assets/javascripts/profile/components/snippets_tab.vue
deleted file mode 100644
index d64c5b900a5..00000000000
--- a/app/assets/javascripts/profile/components/snippets_tab.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-<script>
-import { GlTab } from '@gitlab/ui';
-import { s__ } from '~/locale';
-
-export default {
- i18n: {
- title: s__('UserProfile|Snippets'),
- },
- components: { GlTab },
-};
-</script>
-
-<template>
- <gl-tab :title="$options.i18n.title">
- <!-- placeholder -->
- </gl-tab>
-</template>
diff --git a/app/assets/javascripts/profile/constants.js b/app/assets/javascripts/profile/constants.js
index e19994c6784..9d3dcd648a8 100644
--- a/app/assets/javascripts/profile/constants.js
+++ b/app/assets/javascripts/profile/constants.js
@@ -5,3 +5,5 @@ export const CALENDAR_PERIOD_12_MONTHS = 12;
* (see activity_calendar.js)
*/
export const OVERVIEW_CALENDAR_BREAKPOINT = 918;
+
+export const SNIPPET_MAX_LIST_COUNT = 20;
diff --git a/app/assets/javascripts/profile/index.js b/app/assets/javascripts/profile/index.js
index 101e52c873e..894912d8e4b 100644
--- a/app/assets/javascripts/profile/index.js
+++ b/app/assets/javascripts/profile/index.js
@@ -13,10 +13,22 @@ export const initProfileTabs = () => {
if (!el) return false;
- const { followees, followers, userCalendarPath, utcOffset, userId } = el.dataset;
+ const {
+ followees,
+ followers,
+ userCalendarPath,
+ utcOffset,
+ userId,
+ snippetsEmptyState,
+ } = el.dataset;
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
return new Vue({
el,
+ apolloProvider,
name: 'ProfileRoot',
provide: {
followees: parseInt(followers, 10),
@@ -24,6 +36,7 @@ export const initProfileTabs = () => {
userCalendarPath,
utcOffset,
userId,
+ snippetsEmptyState,
},
render(createElement) {
return createElement(ProfileTabs);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index f120680b440..fad0830388e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -146,6 +146,8 @@ export default {
AddedCommitMessage,
RelatedLinks,
HelpPopover,
+ AiCommitMessage: () =>
+ import('ee_component/vue_merge_request_widget/components/ai_commit_message.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -502,6 +504,10 @@ export default {
this.squashCommitMessage = val;
this.squashCommitMessageIsTouched = true;
},
+ appendCommitMessage(val) {
+ this.commitMessage = `${this.commitMessage}\n\n${val}`;
+ this.commitMessageIsTouched = true;
+ },
},
i18n: {
mergeCommitTemplateHintText: s__(
@@ -596,7 +602,15 @@ export default {
input-id="merge-message-edit"
class="gl-m-0! gl-p-0!"
@input="setCommitMessage"
- />
+ >
+ <template #header>
+ <ai-commit-message
+ v-if="mr.aiCommitMessageEnabled"
+ :id="mr.id"
+ @update="appendCommitMessage"
+ />
+ </template>
+ </commit-edit>
<li class="gl-m-0! gl-p-0!">
<p class="form-text text-muted">
<gl-sprintf :message="commitTemplateHintText">
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index c6edb16e5a1..c5dc45e2138 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -188,7 +188,8 @@ module UsersHelper
followers: user.followers.count,
user_calendar_path: user_calendar_path(user, :json),
utc_offset: local_timezone_instance(user.timezone).now.utc_offset,
- user_id: user.id
+ user_id: user.id,
+ snippets_empty_state: image_path('illustrations/empty-state/empty-snippets-md.svg')
}
end
diff --git a/app/services/concerns/search/filter.rb b/app/services/concerns/search/filter.rb
new file mode 100644
index 00000000000..c358f49eef1
--- /dev/null
+++ b/app/services/concerns/search/filter.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Search
+ module Filter
+ private
+
+ def filters
+ { state: params[:state], confidential: params[:confidential] }
+ end
+ end
+end
+
+Search::Filter.prepend_mod
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index cee59360b4b..2d4952dacfd 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -2,6 +2,7 @@
module Search
class GlobalService
+ include Search::Filter
include Gitlab::Utils::StrongMemoize
ALLOWED_SCOPES = %w(issues merge_requests milestones users).freeze
@@ -19,7 +20,7 @@ module Search
projects,
order_by: params[:order_by],
sort: params[:sort],
- filters: { state: params[:state], confidential: params[:confidential] })
+ filters: filters)
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/services/search/group_service.rb b/app/services/search/group_service.rb
index daed0df83f3..fa80a6ecf58 100644
--- a/app/services/search/group_service.rb
+++ b/app/services/search/group_service.rb
@@ -18,7 +18,7 @@ module Search
group: group,
order_by: params[:order_by],
sort: params[:sort],
- filters: { state: params[:state], confidential: params[:confidential] }
+ filters: filters
)
end
diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb
index 6acc32ea0a8..d30c500df14 100644
--- a/app/services/search/project_service.rb
+++ b/app/services/search/project_service.rb
@@ -2,8 +2,8 @@
module Search
class ProjectService
+ include Search::Filter
include Gitlab::Utils::StrongMemoize
-
ALLOWED_SCOPES = %w(notes issues merge_requests milestones wiki_blobs commits users).freeze
attr_accessor :project, :current_user, :params
@@ -21,7 +21,7 @@ module Search
repository_ref: params[:repository_ref],
order_by: params[:order_by],
sort: params[:sort],
- filters: { confidential: params[:confidential], state: params[:state] }
+ filters: filters
)
end
diff --git a/config/feature_flags/development/generate_commit_message_flag.yml b/config/feature_flags/development/generate_commit_message_flag.yml
new file mode 100644
index 00000000000..b0655fd9929
--- /dev/null
+++ b/config/feature_flags/development/generate_commit_message_flag.yml
@@ -0,0 +1,8 @@
+---
+name: generate_commit_message_flag
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120259
+rollout_issue_url:
+milestone: '16.0'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/config/feature_flags/development/generate_commit_message_vertex.yml b/config/feature_flags/development/generate_commit_message_vertex.yml
new file mode 100644
index 00000000000..cee53bc7e71
--- /dev/null
+++ b/config/feature_flags/development/generate_commit_message_vertex.yml
@@ -0,0 +1,8 @@
+---
+name: generate_commit_message_vertex
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120259
+rollout_issue_url:
+milestone: '16.0'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index d096174fca3..8db6a3bd6b5 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -789,6 +789,12 @@ Gitlab.ee do
Settings.cron_jobs['sync_seat_link_worker'] ||= {}
Settings.cron_jobs['sync_seat_link_worker']['cron'] ||= "#{rand(60)} #{rand(3..4)} * * * UTC"
Settings.cron_jobs['sync_seat_link_worker']['job_class'] = 'SyncSeatLinkWorker'
+ Settings.cron_jobs['tanuki_bot_recreate_records_worker'] ||= {}
+ Settings.cron_jobs['tanuki_bot_recreate_records_worker']['cron'] ||= '0 5 * * 1,2,3,4,5'
+ Settings.cron_jobs['tanuki_bot_recreate_records_worker']['job_class'] ||= 'Llm::TanukiBot::RecreateRecordsWorker'
+ Settings.cron_jobs['tanuki_bot_remove_previous_records_worker'] ||= {}
+ Settings.cron_jobs['tanuki_bot_remove_previous_records_worker']['cron'] ||= '0 0 * * *'
+ Settings.cron_jobs['tanuki_bot_remove_previous_records_worker']['job_class'] ||= 'Llm::TanukiBot::RemovePreviousRecordsWorker'
Settings.cron_jobs['users_create_statistics_worker'] ||= {}
Settings.cron_jobs['users_create_statistics_worker']['cron'] ||= '2 15 * * *'
Settings.cron_jobs['users_create_statistics_worker']['job_class'] = 'Users::CreateStatisticsWorker'
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 30f630eb39b..e3ee44dcfda 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -311,6 +311,8 @@
- 2
- - llm_completion
- 1
+- - llm_tanuki_bot_update
+ - 1
- - mail_scheduler
- 2
- - mailers
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 103947da992..6cf95675fb4 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1019,6 +1019,7 @@ Input type: `AiActionInput`
| <a id="mutationaiactionclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationaiactionexplaincode"></a>`explainCode` | [`AiExplainCodeInput`](#aiexplaincodeinput) | Input for explain_code AI action. |
| <a id="mutationaiactionexplainvulnerability"></a>`explainVulnerability` | [`AiExplainVulnerabilityInput`](#aiexplainvulnerabilityinput) | Input for explain_vulnerability AI action. |
+| <a id="mutationaiactiongeneratecommitmessage"></a>`generateCommitMessage` | [`AiGenerateCommitMessageInput`](#aigeneratecommitmessageinput) | Input for generate_commit_message AI action. |
| <a id="mutationaiactiongeneratedescription"></a>`generateDescription` | [`AiGenerateDescriptionInput`](#aigeneratedescriptioninput) | Input for generate_description AI action. |
| <a id="mutationaiactiongeneratetestfile"></a>`generateTestFile` | [`GenerateTestFileInput`](#generatetestfileinput) | Input for generate_test_file AI action. |
| <a id="mutationaiactionmarkupformat"></a>`markupFormat` | [`MarkupFormat`](#markupformat) | Indicates the response format. |
@@ -19206,7 +19207,7 @@ Represents vulnerability finding of a security report on the pipeline.
| <a id="pipelinesecurityreportfindinglocation"></a>`location` | [`VulnerabilityLocation`](#vulnerabilitylocation) | Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability. |
| <a id="pipelinesecurityreportfindingmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request that fixes the vulnerability. |
| <a id="pipelinesecurityreportfindingproject"></a>`project` | [`Project`](#project) | Project on which the vulnerability finding was found. |
-| <a id="pipelinesecurityreportfindingprojectfingerprint"></a>`projectFingerprint` **{warning-solid}** | [`String`](#string) | **Introduced** in 16.0. This feature is an Experiment. It can be changed or removed at any time. Fingerprint of the vulnerability finding. |
+| <a id="pipelinesecurityreportfindingprojectfingerprint"></a>`projectFingerprint` **{warning-solid}** | [`String`](#string) | **Deprecated** in 16.1. Use uuid instead. |
| <a id="pipelinesecurityreportfindingremediations"></a>`remediations` | [`[VulnerabilityRemediationType!]`](#vulnerabilityremediationtype) | Remediations of the security report finding. |
| <a id="pipelinesecurityreportfindingreporttype"></a>`reportType` | [`VulnerabilityReportType`](#vulnerabilityreporttype) | Type of the security report that found the vulnerability finding. |
| <a id="pipelinesecurityreportfindingscanner"></a>`scanner` | [`VulnerabilityScanner`](#vulnerabilityscanner) | Scanner metadata for the vulnerability. |
@@ -27618,6 +27619,14 @@ see the associated mutation type above.
| ---- | ---- | ----------- |
| <a id="aiexplainvulnerabilityinputresourceid"></a>`resourceId` | [`AiModelID!`](#aimodelid) | Global ID of the resource to mutate. |
+### `AiGenerateCommitMessageInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="aigeneratecommitmessageinputresourceid"></a>`resourceId` | [`AiModelID!`](#aimodelid) | Global ID of the resource to mutate. |
+
### `AiGenerateDescriptionInput`
#### Arguments
diff --git a/doc/architecture/blueprints/ci_pipeline_components/img/catalogs.png b/doc/architecture/blueprints/ci_pipeline_components/img/catalogs.png
new file mode 100644
index 00000000000..9353c5266e5
--- /dev/null
+++ b/doc/architecture/blueprints/ci_pipeline_components/img/catalogs.png
Binary files differ
diff --git a/doc/architecture/blueprints/ci_pipeline_components/index.md b/doc/architecture/blueprints/ci_pipeline_components/index.md
index ff4604b61bf..79660dcaac2 100644
--- a/doc/architecture/blueprints/ci_pipeline_components/index.md
+++ b/doc/architecture/blueprints/ci_pipeline_components/index.md
@@ -597,6 +597,29 @@ For example: index the content of `spec:` section for CI components.
See an [example of development workflow](dev_workflow.md) for a components repository.
+## Availability of CI catalog as a feature
+
+We plan to introduce 2 features of CI catalog as separate views:
+
+1. **Namespace Catalog (GitLab Ultimate):** allows organizations to share and discover catalog resources
+ created inside the top-level namespace.
+ Users will be able to access the Namespace Catalog from a project or subgroup inside the top-level
+ namespace.
+1. **Community Catalog (GitLab free):** allows anyone in a GitLab instance to share and discover catalog
+ resources. The Community Catalog presents only resources/projects that are public.
+
+If a resource in a Namespace Catalog is made public (changing the project's visibility) the resource is
+available in both Namespace Catalog (because it comes from there) as well as the Community Catalog
+(because it's public).
+
+![Namespace and Community Catalogs](img/catalogs.png)
+
+There is only 1 CI catalog. The Namespace and Community Catalogs are different views of the CI catalog.
+
+Project admins are responsible for setting the project private or public.
+The CI Catalog should not provide security functionalities like prevent projects from appearing in the Community Catalog.
+If the project is public it's visible to the world anyway.
+
## Note about future resource types
In the future, to support multiple types of resources in the Catalog we could
diff --git a/doc/user/product_analytics/index.md b/doc/user/product_analytics/index.md
index 1c6a241d5f4..befde99cc37 100644
--- a/doc/user/product_analytics/index.md
+++ b/doc/user/product_analytics/index.md
@@ -10,12 +10,18 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - `cube_api_proxy` revised to only reference the [Product Analytics API](../../api/product_analytics.md) in GitLab 15.6.
> - `cube_api_proxy` removed and replaced with `product_analytics_internal_preview` in GitLab 15.10.
> - `product_analytics_internal_preview` replaced with `product_analytics_dashboards` in GitLab 15.11.
+> - Snowplow integration introduced in GitLab 15.11 [with a flag](../../administration/feature_flags.md) named `product_analytics_snowplow_support`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `product_analytics_dashboards`.
On GitLab.com, this feature is not available.
This feature is not ready for production use.
+FLAG:
+On self-managed GitLab, by default the Snowplow integration is not available. To make it available per project or for your entire instance, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `product_analytics_snowplow_support`.
+On GitLab.com, this feature is not available.
+This feature is not ready for production use.
+
This page is a work in progress, and we're updating the information as we add more features.
For more information, see the [group direction page](https://about.gitlab.com/direction/analytics/product-analytics/).
@@ -165,6 +171,52 @@ create a `line_chart.yaml` file with the following required fields:
- data
- options
+## Dashboards editor
+
+> Introduced in GitLab 16.1 [with a flag](../../administration/feature_flags.md) named `combined_analytics_dashboards_editor`. Disabled by default.
+
+FLAG:
+On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `combined_analytics_dashboards_editor`.
+On GitLab.com, this feature is not available.
+This feature is not ready for production use.
+
+NOTE:
+This feature does not work in conjunction with the `product_analytics_snowplow_support` feature flag.
+
+You can use the dashboards editor to:
+
+- Create dashboards
+- Rename dashboards
+- Add visualizations to new and existing dashboards
+- Resize or move panels within dashboards
+
+### Create a dashboard
+
+To create a dashboard:
+
+1. On the top bar, select **Main menu > Projects** and find your project.
+1. On the left sidebar, select **Analytics > Dashboards**.
+1. Select **New dashboard**.
+1. In the **New dashboard** input, enter the name of the dashboard.
+1. From the **Add visualizations** list on the right, select the visualizations to add to the dashboard.
+1. Optional. Drag or resize the selected visualizations how you prefer.
+1. Select **Save**.
+
+### Edit a dashboard
+
+You can rename your created dashboards and add or resize visualizations within them.
+
+To edit an existing dashboard:
+
+1. On the top bar, select **Main menu > Projects** and find your project.
+1. On the left sidebar, select **Analytics > Dashboards**.
+1. From the list of available dashboards, select the dashboard you want to edit.
+1. Select **Edit**.
+1. Optional. Change the name of the dashboard.
+1. Optional. From the **Add visualizations** list on the right, select other visualizations to add to the dashboard.
+1. Optional. In the dashboard, select a visualization and drag or resize it how you prefer.
+1. Select **Save**.
+
## Funnel analysis
Use funnel analysis to understand the flow of users through your application, and where
diff --git a/lib/gitlab/avatar_cache.rb b/lib/gitlab/avatar_cache.rb
index ed00a279299..2bb3254ebef 100644
--- a/lib/gitlab/avatar_cache.rb
+++ b/lib/gitlab/avatar_cache.rb
@@ -66,9 +66,13 @@ module Gitlab
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
if ::Feature.enabled?(:use_pipeline_over_multikey)
- redis.pipelined do |pipeline|
- keys.each { |key| pipeline.unlink(key) }
- end.sum
+ expired_count = 0
+ keys.each_slice(1000) do |subset|
+ expired_count += redis.pipelined do |pipeline|
+ subset.each { |key| pipeline.unlink(key) }
+ end.sum
+ end
+ expired_count
else
redis.unlink(*keys)
end
diff --git a/lib/gitlab/error_tracking/processor/grpc_error_processor.rb b/lib/gitlab/error_tracking/processor/grpc_error_processor.rb
index c141398bee0..ab0df39e512 100644
--- a/lib/gitlab/error_tracking/processor/grpc_error_processor.rb
+++ b/lib/gitlab/error_tracking/processor/grpc_error_processor.rb
@@ -6,8 +6,7 @@ module Gitlab
module GrpcErrorProcessor
extend Gitlab::ErrorTracking::Processor::Concerns::ProcessesExceptions
- # Braces added by gRPC Ruby code: https://github.com/grpc/grpc/blob/0e38b075ffff72ab2ad5326e3f60ba6dcc234f46/src/ruby/lib/grpc/errors.rb#L46
- DEBUG_ERROR_STRING_REGEX = RE2('(.*) debug_error_string:\{(.*)\}')
+ DEBUG_ERROR_STRING_REGEX = RE2('(.*) debug_error_string:(.*)')
class << self
def call(event)
diff --git a/lib/gitlab/reactive_cache_set_cache.rb b/lib/gitlab/reactive_cache_set_cache.rb
index dc13bb927e6..242c69ab057 100644
--- a/lib/gitlab/reactive_cache_set_cache.rb
+++ b/lib/gitlab/reactive_cache_set_cache.rb
@@ -11,17 +11,19 @@ module Gitlab
end
def clear_cache!(key)
- use_pipeline = ::Feature.enabled?(:use_pipeline_over_multikey)
-
with do |redis|
keys = read(key).map { |value| "#{cache_namespace}:#{value}" }
keys << cache_key(key)
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.pipelined do |pipeline|
- if use_pipeline
- keys.each { |key| pipeline.unlink(key) }
- else
+ if ::Feature.enabled?(:use_pipeline_over_multikey)
+ keys.each_slice(1000) do |subset|
+ redis.pipelined do |pipeline|
+ subset.each { |key| pipeline.unlink(key) }
+ end
+ end
+ else
+ redis.pipelined do |pipeline|
keys.each_slice(1000) { |subset| pipeline.unlink(*subset) }
end
end
diff --git a/lib/gitlab/set_cache.rb b/lib/gitlab/set_cache.rb
index 623b254c4e0..09e07add293 100644
--- a/lib/gitlab/set_cache.rb
+++ b/lib/gitlab/set_cache.rb
@@ -23,9 +23,13 @@ module Gitlab
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
if ::Feature.enabled?(:use_pipeline_over_multikey)
- redis.pipelined do |pipeline|
- keys_to_expire.each { |key| pipeline.unlink(key) }
- end.sum
+ expired_count = 0
+ keys_to_expire.each_slice(1000) do |subset|
+ expired_count += redis.pipelined do |pipeline|
+ subset.each { |key| pipeline.unlink(key) }
+ end.sum
+ end
+ expired_count
else
redis.unlink(*keys_to_expire)
end
diff --git a/lib/gitlab/usage_data_counters/known_events/workspaces.yml b/lib/gitlab/usage_data_counters/known_events/workspaces.yml
new file mode 100644
index 00000000000..8a96524b167
--- /dev/null
+++ b/lib/gitlab/usage_data_counters/known_events/workspaces.yml
@@ -0,0 +1,5 @@
+- name: users_updating_workspaces
+ aggregation: weekly
+
+- name: users_creating_workspaces
+ aggregation: weekly
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c6be174be47..8f709205234 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -11050,6 +11050,9 @@ msgstr ""
msgid "Commit message (optional)"
msgstr ""
+msgid "Commit message generated by AI"
+msgstr ""
+
msgid "Commit statistics for %{ref} %{start_time} - %{end_time}"
msgstr ""
@@ -12592,6 +12595,9 @@ msgstr ""
msgid "Create %{workspace} label"
msgstr ""
+msgid "Create AI-generated commit message"
+msgstr ""
+
msgid "Create LLM-generated summary from diff(s)"
msgstr ""
@@ -23815,6 +23821,9 @@ msgstr ""
msgid "Input the remote repository URL"
msgstr ""
+msgid "Insert"
+msgstr ""
+
msgid "Insert a %{rows}×%{cols} table"
msgstr ""
@@ -45774,6 +45783,9 @@ msgstr ""
msgid "There was an error gathering the chart data"
msgstr ""
+msgid "There was an error generating commit message."
+msgstr ""
+
msgid "There was an error getting the epic participants."
msgstr ""
@@ -48918,6 +48930,9 @@ msgstr ""
msgid "UserList|created %{timeago}"
msgstr ""
+msgid "UserProfiles|No snippets found."
+msgstr ""
+
msgid "UserProfile|Activity"
msgstr ""
diff --git a/package.json b/package.json
index 5f798734b0d..3b4a061d752 100644
--- a/package.json
+++ b/package.json
@@ -56,7 +56,7 @@
"@gitlab/cluster-client": "^1.2.0",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.2.0",
- "@gitlab/svgs": "3.46.0",
+ "@gitlab/svgs": "3.47.0",
"@gitlab/ui": "62.10.0",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20230511143809",
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index a812b94957f..1dcb41de816 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -141,8 +141,8 @@ GEM
google-apis-core (>= 0.7.2, < 2.a)
google-apis-sqladmin_v1beta4 (0.36.0)
google-apis-core (>= 0.7.2, < 2.a)
- google-apis-storage_v1 (0.18.0)
- google-apis-core (>= 0.7, < 2.a)
+ google-apis-storage_v1 (0.19.0)
+ google-apis-core (>= 0.9.0, < 2.a)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
googleauth (1.2.0)
diff --git a/qa/qa/tools/migrate_influx_data_to_gcs.rb b/qa/qa/tools/migrate_influx_data_to_gcs.rb
new file mode 100644
index 00000000000..3313131275a
--- /dev/null
+++ b/qa/qa/tools/migrate_influx_data_to_gcs.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require 'csv'
+require "fog/google"
+
+module QA
+ module Tools
+ class MigrateInfluxDataToGcs
+ include Support::InfluxdbTools
+
+ # Google Cloud Storage bucket from which Snowpipe would pull data into Snowflake
+ QA_GCS_BUCKET_NAME = ENV["QA_GCS_BUCKET_NAME"] || raise("Missing QA_GCS_BUCKET_NAME env variable")
+ QA_GCS_PROJECT_ID = ENV["QA_GCS_PROJECT_ID"] || raise("Missing QA_GCS_PROJECT_ID env variable")
+ QA_GCS_JSON_KEY_FILE = ENV["QA_GCS_JSON_KEY_FILE"] || raise("Missing QA_GCS_JSON_KEY_FILE env variable")
+ INFLUX_STATS_TYPE = %w[test-stats fabrication-stats].freeze
+ INFLUX_BUCKETS = [Support::InfluxdbTools::INFLUX_TEST_METRICS_BUCKET,
+ Support::InfluxdbTools::INFLUX_MAIN_TEST_METRICS_BUCKET].freeze
+ TEST_STATS_FIELDS = %w[id testcase file_path name product_group stage job_id job_name
+ job_url pipeline_id pipeline_url merge_request merge_request_iid smoke reliable quarantined
+ retried retry_attempts run_time run_type status ui_fabrication api_fabrication total_fabrication].freeze
+ FABRICATION_STATS_FIELDS = %w[timestamp resource fabrication_method http_method run_type
+ merge_request fabrication_time info job_url].freeze
+
+ def initialize(range)
+ @range = range.to_i
+ end
+
+ # Run Influx Migrator
+ # @param [Integer] the last x hours for which data is required
+ # @return [void]
+ def self.run(range: 1)
+ migrator = new(range)
+
+ migrator.migrate_data
+ end
+
+ # Fetch data from Influx DB , store as CSV and upload to GCS
+ # @return [void]
+ def migrate_data
+ INFLUX_BUCKETS.each do |bucket|
+ INFLUX_STATS_TYPE.each do |stats_type|
+ if bucket == Support::InfluxdbTools::INFLUX_MAIN_TEST_METRICS_BUCKET && stats_type == "fabrication-stats"
+ break
+ end
+
+ file_name = "#{bucket.end_with?('main') ? 'main' : 'all'}-#{stats_type}_#{Time.now.to_i}.csv"
+ influx_to_csv(bucket, stats_type, file_name)
+
+ # Upload to Google Cloud Storage
+ upload_to_gcs(QA_GCS_BUCKET_NAME, file_name)
+ end
+ end
+ end
+
+ private
+
+ # FluxQL query used to fetch data
+ # @param [String] influx bucket to fetch data
+ # @param [String] Type of data to fetch
+ # @return [String] query string
+ def query(influx_bucket, stats_type)
+ <<~QUERY
+ from(bucket: "#{influx_bucket}")
+ |> range(start: -#{@range}h)
+ |> filter(fn: (r) => r["_measurement"] == "#{stats_type}")
+ |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
+ |> drop(columns: ["_start", "_stop", "result", "table", "_time", "_measurement"])
+ QUERY
+ end
+
+ # Query InfluxDB and store in CSV
+ # @param [String] influx bucket to fetch data
+ # @param [String] Type of data to fetch
+ # @param [String] CSV filename to store data
+ # @return void
+ def influx_to_csv(influx_bucket, stats_type, data_file_name)
+ all_runs = query_api.query(query: query(influx_bucket, stats_type))
+ CSV.open(data_file_name, "wb") do |csv|
+ stats_array = stats_type == "test-stats" ? TEST_STATS_FIELDS : FABRICATION_STATS_FIELDS
+ csv << stats_array.flatten
+ all_runs.each do |table|
+ table.records.each do |record|
+ csv << stats_array.map { |key| record.values[key] }
+ end
+ end
+ QA::Runtime::Logger.info("File #{data_file_name} contains #{all_runs.count} rows")
+ end
+ end
+
+ # Fetch GCS Credentials
+ # @return [Hash] GCS Credentials
+ def gcs_credentials
+ json_key = ENV["QA_GCS_JSON_KEY_FILE"] || raise(
+ "QA_GCS_JSON_KEY_FILE env variable is required!"
+ )
+ return { google_json_key_location: json_key } if File.exist?(json_key)
+
+ { google_json_key_string: json_key }
+ end
+
+ # Upload file to GCS
+ # @param [String] bucket to be uploaded to
+ # @param [String] path of file to be uploaded
+ # return void
+ def upload_to_gcs(bucket, file_path)
+ client = Fog::Storage::Google.new(google_project: QA_GCS_PROJECT_ID, **gcs_credentials)
+ file = client.put_object(bucket, file_path, File.new(file_path, "r"))
+ QA::Runtime::Logger.info("File #{file_path} uploaded to gs://#{bucket}/#{file.name}")
+ end
+ end
+ end
+end
diff --git a/qa/tasks/migrate_influx_data_to_gcs.rake b/qa/tasks/migrate_influx_data_to_gcs.rake
new file mode 100644
index 00000000000..d6ac6e962f4
--- /dev/null
+++ b/qa/tasks/migrate_influx_data_to_gcs.rake
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+desc "Migrate the test results data from InfluxDB to GCS to visualise in Sisense/Tableau"
+task :influx_to_gcs, [:range] do |_task, args|
+ QA::Tools::MigrateInfluxDataToGcs.run(**args)
+end
diff --git a/spec/frontend/profile/components/profile_tabs_spec.js b/spec/frontend/profile/components/profile_tabs_spec.js
index 80a1ff422ab..f3dda2e205f 100644
--- a/spec/frontend/profile/components/profile_tabs_spec.js
+++ b/spec/frontend/profile/components/profile_tabs_spec.js
@@ -10,7 +10,7 @@ import GroupsTab from '~/profile/components/groups_tab.vue';
import ContributedProjectsTab from '~/profile/components/contributed_projects_tab.vue';
import PersonalProjectsTab from '~/profile/components/personal_projects_tab.vue';
import StarredProjectsTab from '~/profile/components/starred_projects_tab.vue';
-import SnippetsTab from '~/profile/components/snippets_tab.vue';
+import SnippetsTab from '~/profile/components/snippets/snippets_tab.vue';
import FollowersTab from '~/profile/components/followers_tab.vue';
import FollowingTab from '~/profile/components/following_tab.vue';
import waitForPromises from 'helpers/wait_for_promises';
diff --git a/spec/frontend/profile/components/snippets/snippet_row_spec.js b/spec/frontend/profile/components/snippets/snippet_row_spec.js
new file mode 100644
index 00000000000..0c0e262bd9a
--- /dev/null
+++ b/spec/frontend/profile/components/snippets/snippet_row_spec.js
@@ -0,0 +1,31 @@
+import SnippetRow from '~/profile/components/snippets/snippet_row.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { MOCK_USER, MOCK_SNIPPET } from 'jest/profile/mock_data';
+
+describe('UserProfileSnippetRow', () => {
+ let wrapper;
+
+ const defaultProps = {
+ userInfo: MOCK_USER,
+ snippet: MOCK_SNIPPET,
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(SnippetRow, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders snippet title', () => {
+ expect(wrapper.text()).toBe(MOCK_SNIPPET.title);
+ });
+ });
+});
diff --git a/spec/frontend/profile/components/snippets/snippets_tab_spec.js b/spec/frontend/profile/components/snippets/snippets_tab_spec.js
new file mode 100644
index 00000000000..47e2fbcf2c0
--- /dev/null
+++ b/spec/frontend/profile/components/snippets/snippets_tab_spec.js
@@ -0,0 +1,162 @@
+import { GlEmptyState, GlKeysetPagination } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPENAME_USER } from '~/graphql_shared/constants';
+import { SNIPPET_MAX_LIST_COUNT } from '~/profile/constants';
+import SnippetsTab from '~/profile/components/snippets/snippets_tab.vue';
+import SnippetRow from '~/profile/components/snippets/snippet_row.vue';
+import getUserSnippets from '~/profile/components/graphql/get_user_snippets.query.graphql';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import {
+ MOCK_USER,
+ MOCK_SNIPPETS_EMPTY_STATE,
+ MOCK_USER_SNIPPETS_RES,
+ MOCK_USER_SNIPPETS_PAGINATION_RES,
+ MOCK_USER_SNIPPETS_EMPTY_RES,
+} from 'jest/profile/mock_data';
+
+Vue.use(VueApollo);
+
+describe('UserProfileSnippetsTab', () => {
+ let wrapper;
+
+ let queryHandlerMock = jest.fn().mockResolvedValue(MOCK_USER_SNIPPETS_RES);
+
+ const createComponent = () => {
+ const apolloProvider = createMockApollo([[getUserSnippets, queryHandlerMock]]);
+
+ wrapper = shallowMountExtended(SnippetsTab, {
+ apolloProvider,
+ provide: {
+ userId: MOCK_USER.id,
+ snippetsEmptyState: MOCK_SNIPPETS_EMPTY_STATE,
+ },
+ });
+ };
+
+ const findSnippetRows = () => wrapper.findAllComponents(SnippetRow);
+ const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findGlKeysetPagination = () => wrapper.findComponent(GlKeysetPagination);
+
+ describe('when user has no snippets', () => {
+ beforeEach(async () => {
+ queryHandlerMock = jest.fn().mockResolvedValue(MOCK_USER_SNIPPETS_EMPTY_RES);
+ createComponent();
+
+ await nextTick();
+ });
+
+ it('does not render snippet row', () => {
+ expect(findSnippetRows().exists()).toBe(false);
+ });
+
+ it('does render empty state with correct svg', () => {
+ expect(findGlEmptyState().exists()).toBe(true);
+ expect(findGlEmptyState().attributes('svgpath')).toBe(MOCK_SNIPPETS_EMPTY_STATE);
+ });
+ });
+
+ describe('when snippets returns an error', () => {
+ beforeEach(async () => {
+ queryHandlerMock = jest.fn().mockRejectedValue({ errors: [] });
+ createComponent();
+
+ await nextTick();
+ });
+
+ it('does not render snippet row', () => {
+ expect(findSnippetRows().exists()).toBe(false);
+ });
+
+ it('does render empty state with correct svg', () => {
+ expect(findGlEmptyState().exists()).toBe(true);
+ expect(findGlEmptyState().attributes('svgpath')).toBe(MOCK_SNIPPETS_EMPTY_STATE);
+ });
+ });
+
+ describe('when snippets are returned', () => {
+ beforeEach(async () => {
+ queryHandlerMock = jest.fn().mockResolvedValue(MOCK_USER_SNIPPETS_RES);
+ createComponent();
+
+ await nextTick();
+ });
+
+ it('renders a snippet row for each snippet', () => {
+ expect(findSnippetRows().exists()).toBe(true);
+ expect(findSnippetRows().length).toBe(MOCK_USER_SNIPPETS_RES.data.user.snippets.nodes.length);
+ });
+
+ it('does not render empty state', () => {
+ expect(findGlEmptyState().exists()).toBe(false);
+ });
+
+ it('adds bottom border when snippet is not last in list', () => {
+ expect(findSnippetRows().at(0).classes('gl-border-b')).toBe(true);
+ });
+
+ it('does not add bottom border when snippet is last in list', () => {
+ expect(
+ findSnippetRows()
+ .at(MOCK_USER_SNIPPETS_RES.data.user.snippets.nodes.length - 1)
+ .classes('gl-border-b'),
+ ).toBe(false);
+ });
+ });
+
+ describe('Snippet Pagination', () => {
+ describe('when user has one page of snippets', () => {
+ beforeEach(async () => {
+ queryHandlerMock = jest.fn().mockResolvedValue(MOCK_USER_SNIPPETS_RES);
+ createComponent();
+
+ await nextTick();
+ });
+
+ it('does not render pagination', () => {
+ expect(findGlKeysetPagination().exists()).toBe(false);
+ });
+ });
+
+ describe('when user has multiple pages of snippets', () => {
+ beforeEach(async () => {
+ queryHandlerMock = jest.fn().mockResolvedValue(MOCK_USER_SNIPPETS_PAGINATION_RES);
+ createComponent();
+
+ await nextTick();
+ });
+
+ it('does render pagination', () => {
+ expect(findGlKeysetPagination().exists()).toBe(true);
+ });
+
+ it('when nextPage is clicked', async () => {
+ findGlKeysetPagination().vm.$emit('next');
+
+ await nextTick();
+
+ expect(queryHandlerMock).toHaveBeenCalledWith({
+ id: convertToGraphQLId(TYPENAME_USER, MOCK_USER.id),
+ first: SNIPPET_MAX_LIST_COUNT,
+ last: null,
+ afterToken: MOCK_USER_SNIPPETS_RES.data.user.snippets.pageInfo.endCursor,
+ });
+ });
+
+ it('when previousPage is clicked', async () => {
+ findGlKeysetPagination().vm.$emit('prev');
+
+ await nextTick();
+
+ expect(queryHandlerMock).toHaveBeenCalledWith({
+ id: convertToGraphQLId(TYPENAME_USER, MOCK_USER.id),
+ first: null,
+ last: SNIPPET_MAX_LIST_COUNT,
+ beforeToken: MOCK_USER_SNIPPETS_RES.data.user.snippets.pageInfo.startCursor,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/profile/components/snippets_tab_spec.js b/spec/frontend/profile/components/snippets_tab_spec.js
deleted file mode 100644
index 1306757314c..00000000000
--- a/spec/frontend/profile/components/snippets_tab_spec.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { GlTab } from '@gitlab/ui';
-
-import { s__ } from '~/locale';
-import SnippetsTab from '~/profile/components/snippets_tab.vue';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-
-describe('SnippetsTab', () => {
- let wrapper;
-
- const createComponent = () => {
- wrapper = shallowMountExtended(SnippetsTab);
- };
-
- it('renders `GlTab` and sets `title` prop', () => {
- createComponent();
-
- expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Snippets'));
- });
-});
diff --git a/spec/frontend/profile/mock_data.js b/spec/frontend/profile/mock_data.js
index 7106ea84619..74387af5be7 100644
--- a/spec/frontend/profile/mock_data.js
+++ b/spec/frontend/profile/mock_data.js
@@ -20,3 +20,79 @@ export const userCalendarResponse = {
'2023-02-06': 2,
'2023-02-07': 2,
};
+
+export const MOCK_SNIPPETS_EMPTY_STATE = 'illustrations/empty-state/empty-snippets-md.svg';
+
+export const MOCK_USER = {
+ id: '1',
+ avatarUrl: 'https://www.gravatar.com/avatar/test',
+ name: 'Test User',
+ username: 'test',
+};
+
+const getMockSnippet = (id) => {
+ return {
+ id: `gid://gitlab/PersonalSnippet/${id}`,
+ title: `Test snippet ${id}`,
+ visibilityLevel: 'public',
+ webUrl: `http://gitlab.com/-/snippets/${id}`,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ blobs: {
+ nodes: [
+ {
+ name: 'test.txt',
+ },
+ ],
+ },
+ commenters: {
+ nodes: [
+ {
+ id: 'git://gitlab/User/1',
+ },
+ ],
+ },
+ };
+};
+
+const MOCK_PAGE_INFO = {
+ startCursor: 'asdfqwer',
+ endCursor: 'reqwfdsa',
+ __typename: 'PageInfo',
+};
+
+const getMockSnippetRes = (hasPagination) => {
+ return {
+ data: {
+ user: {
+ ...MOCK_USER,
+ snippets: {
+ pageInfo: {
+ ...MOCK_PAGE_INFO,
+ hasNextPage: hasPagination,
+ hasPreviousPage: hasPagination,
+ },
+ nodes: [getMockSnippet(1), getMockSnippet(2)],
+ },
+ },
+ },
+ };
+};
+
+export const MOCK_SNIPPET = getMockSnippet(1);
+export const MOCK_USER_SNIPPETS_RES = getMockSnippetRes(false);
+export const MOCK_USER_SNIPPETS_PAGINATION_RES = getMockSnippetRes(true);
+export const MOCK_USER_SNIPPETS_EMPTY_RES = {
+ data: {
+ user: {
+ ...MOCK_USER,
+ snippets: {
+ pageInfo: {
+ endCursor: null,
+ startCursor: null,
+ },
+ nodes: [],
+ },
+ },
+ },
+};
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
index 0a259b80219..74b90ce3c6e 100644
--- a/spec/helpers/users_helper_spec.rb
+++ b/spec/helpers/users_helper_spec.rb
@@ -502,12 +502,13 @@ RSpec.describe UsersHelper do
end
it 'returns expected hash' do
- expect(helper.user_profile_tabs_app_data(user)).to eq({
+ expect(helper.user_profile_tabs_app_data(user)).to match({
followees: 3,
followers: 2,
user_calendar_path: '/users/root/calendar.json',
utc_offset: 0,
- user_id: user.id
+ user_id: user.id,
+ snippets_empty_state: match_asset_path('illustrations/empty-state/empty-snippets-md.svg')
})
end
end
diff --git a/spec/lib/gitlab/avatar_cache_spec.rb b/spec/lib/gitlab/avatar_cache_spec.rb
index a57d811edaf..db2fcf9d140 100644
--- a/spec/lib/gitlab/avatar_cache_spec.rb
+++ b/spec/lib/gitlab/avatar_cache_spec.rb
@@ -100,6 +100,16 @@ RSpec.describe Gitlab::AvatarCache, :clean_gitlab_redis_cache do
end
end
+ context 'when deleting over 1000 emails' do
+ it 'deletes in batches of 1000' do
+ Gitlab::Redis::Cache.with do |redis|
+ expect(redis).to receive(:pipelined).twice.and_call_original
+ end
+
+ described_class.delete_by_email(*(Array.new(1001) { |i| i }))
+ end
+ end
+
context 'when feature flag disabled' do
before do
stub_feature_flags(use_pipeline_over_multikey: false)
diff --git a/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb b/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb
index 3399c6dd9f4..33d322d0d44 100644
--- a/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb
+++ b/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor, :sentry, feature_category: :integrations do
+RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor, :sentry do
describe '.call' do
let(:raven_required_options) do
{
diff --git a/spec/lib/gitlab/reactive_cache_set_cache_spec.rb b/spec/lib/gitlab/reactive_cache_set_cache_spec.rb
index a78d15134fa..1a71b23533e 100644
--- a/spec/lib/gitlab/reactive_cache_set_cache_spec.rb
+++ b/spec/lib/gitlab/reactive_cache_set_cache_spec.rb
@@ -68,6 +68,20 @@ RSpec.describe Gitlab::ReactiveCacheSetCache, :clean_gitlab_redis_cache do
it_behaves_like 'clears cache'
end
+ context 'when key size is large' do
+ before do
+ 1001.times { |i| cache.write(cache_prefix, i) }
+ end
+
+ it 'sends multiple pipelines of 1000 unlinks' do
+ Gitlab::Redis::Cache.with do |redis|
+ expect(redis).to receive(:pipelined).twice.and_call_original
+ end
+
+ cache.clear_cache!(cache_prefix)
+ end
+ end
+
it_behaves_like 'clears cache'
end
diff --git a/spec/lib/gitlab/repository_set_cache_spec.rb b/spec/lib/gitlab/repository_set_cache_spec.rb
index 65a50b68c44..df980b50d03 100644
--- a/spec/lib/gitlab/repository_set_cache_spec.rb
+++ b/spec/lib/gitlab/repository_set_cache_spec.rb
@@ -117,6 +117,16 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do
end
end
+ context 'when deleting over 1000 keys' do
+ it 'deletes in batches of 1000' do
+ Gitlab::Redis::RepositoryCache.with do |redis|
+ expect(redis).to receive(:pipelined).twice.and_call_original
+ end
+
+ cache.expire(*(Array.new(1001) { |i| i }))
+ end
+ end
+
context 'when feature flag is disabled' do
before do
stub_feature_flags(use_pipeline_over_multikey: false)
diff --git a/spec/support/helpers/database/multiple_databases_helpers.rb b/spec/support/helpers/database/multiple_databases_helpers.rb
index 3c9a5762c47..806956ca648 100644
--- a/spec/support/helpers/database/multiple_databases_helpers.rb
+++ b/spec/support/helpers/database/multiple_databases_helpers.rb
@@ -69,8 +69,10 @@ module Database
config_model: base_model
)
- delete_from_all_tables!(except: deletion_except_tables)
+ # Delete after migrating so that rows created during migration don't impact other
+ # specs (for example, async foreign key creation rows)
schema_migrate_up!
+ delete_from_all_tables!(except: deletion_except_tables)
end
end
diff --git a/spec/support/shared_examples/lib/gitlab/search_labels_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_labels_filter_shared_examples.rb
new file mode 100644
index 00000000000..b7e408415c3
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/search_labels_filter_shared_examples.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'search results filtered by labels' do
+ let(:project_label) { create(:label, project: project) }
+ let!(:issue_1) { create(:labeled_issue, labels: [project_label], project: project, title: 'foo project') }
+ let!(:unlabeled_issue) { create(:issue, project: project, title: 'foo unlabeled') }
+
+ let(:filters) { { labels: [project_label.id] } }
+
+ before do
+ ensure_elasticsearch_index!
+ end
+
+ subject(:issue_results) { results.objects(scope) }
+
+ it 'filters by labels', :sidekiq_inline do
+ expect(issue_results).to contain_exactly(issue_1)
+ end
+end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 16173e322b6..425bbd9278a 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -345,6 +345,8 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'JiraConnect::SyncProjectWorker' => 3,
'LdapGroupSyncWorker' => 3,
'Licenses::ResetSubmitLicenseUsageDataBannerWorker' => 13,
+ 'Llm::TanukiBot::UpdateWorker' => 1,
+ 'Llm::TanukiBot::RecreateRecordsWorker' => 3,
'MailScheduler::IssueDueWorker' => 3,
'MailScheduler::NotificationServiceWorker' => 3,
'MembersDestroyer::UnassignIssuablesWorker' => 3,
diff --git a/workhorse/go.mod b/workhorse/go.mod
index 20daa9e4843..6a6f756eeef 100644
--- a/workhorse/go.mod
+++ b/workhorse/go.mod
@@ -25,7 +25,7 @@ require (
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
github.com/sirupsen/logrus v1.9.0
github.com/smartystreets/goconvey v1.7.2
- github.com/stretchr/testify v1.8.2
+ github.com/stretchr/testify v1.8.3
gitlab.com/gitlab-org/gitaly/v15 v15.11.0
gitlab.com/gitlab-org/labkit v1.18.0
gocloud.dev v0.29.0
diff --git a/workhorse/go.sum b/workhorse/go.sum
index 6ab1ff99c4c..94c11568369 100644
--- a/workhorse/go.sum
+++ b/workhorse/go.sum
@@ -1862,8 +1862,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
-github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
+github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
diff --git a/yarn.lock b/yarn.lock
index 134e383cc4c..3f8333fbf76 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1110,10 +1110,10 @@
stylelint-declaration-strict-value "1.8.0"
stylelint-scss "4.2.0"
-"@gitlab/svgs@3.46.0":
- version "3.46.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.46.0.tgz#741fea428ce9cac9fd8ccdb65a7b863ab5f7773d"
- integrity sha512-+NEdjNTBCTnJIjQKomf6yasT3ezg8UNBGJVUDf+ZgXgENKbwOjV9ngcVeHQMZM4hDaQSaJx08fagyadCcTw0Ow==
+"@gitlab/svgs@3.47.0":
+ version "3.47.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.47.0.tgz#1a18f008aef1ecb5407688017c3bbdbc597b7ec1"
+ integrity sha512-xP8AyuFYRFmlxtcBYRqCnLmBgMjrACa0mUliRk/hAKUWcXoz/U4vdK69T1DhWalVi4cpUqmi4+rrIWI6fBdzew==
"@gitlab/ui@62.10.0":
version "62.10.0"