summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-04-07 12:10:39 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-04-07 12:10:39 +0000
commit4d5e790175cbd85f4b5bb0a9996efde10a9cad65 (patch)
treeda36b26cfdbd9da8eb40e0ea649672242b98c158
parent6c448c743f157c882ace5291aba21208fa6b011b (diff)
downloadgitlab-ce-4d5e790175cbd85f4b5bb0a9996efde10a9cad65.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/header.js2
-rw-r--r--app/assets/javascripts/nav/components/new_nav_toggle.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu.vue37
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_name_group.vue8
-rw-r--r--app/assets/javascripts/super_sidebar/constants.js5
-rw-r--r--doc/ci/pipelines/cicd_minutes.md7
-rw-r--r--doc/ci/pipelines/merge_request_pipelines.md16
-rw-r--r--doc/integration/jira/configure.md24
-rw-r--r--doc/integration/jira/index.md2
-rw-r--r--doc/integration/jira/jira_cloud_configuration.md26
-rw-r--r--doc/user/permissions.md23
-rw-r--r--lib/gitlab/background_migration/backfill_partitioned_table.rb43
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb37
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/bulk_copy.rb42
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb27
-rw-r--r--lib/gitlab/legacy_github_import/user_formatter.rb11
-rw-r--r--spec/frontend/nav/components/new_nav_toggle_spec.js52
-rw-r--r--spec/frontend/super_sidebar/components/user_menu_spec.js124
-rw-r--r--spec/frontend/super_sidebar/components/user_name_group_spec.js22
-rw-r--r--spec/frontend/super_sidebar/mock_data.js1
-rw-r--r--spec/lib/gitlab/background_migration/backfill_partitioned_table_spec.rb140
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb7
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb21
-rw-r--r--spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb23
25 files changed, 551 insertions, 153 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 24b9b695c69..703c7d9a560 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-c31b9fed97bb01a1790496386ceab8e31e76b1d8
+324d280ab6fda4edaded87f03efaf6f9377be743
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 5a002784937..25a84d17379 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -37,7 +37,7 @@ export function initStatusTriggers() {
const buttonWithinTopNav = topNavbar && topNavbar.contains(setStatusModalTriggerEl);
Tracking.event(undefined, 'click_button', {
label: 'user_edit_status',
- property: buttonWithinTopNav ? 'navigation_top' : undefined,
+ property: buttonWithinTopNav ? 'navigation_top' : 'nav_user_menu',
});
import(
diff --git a/app/assets/javascripts/nav/components/new_nav_toggle.vue b/app/assets/javascripts/nav/components/new_nav_toggle.vue
index 30a415a8cfd..5eb5e5b9b90 100644
--- a/app/assets/javascripts/nav/components/new_nav_toggle.vue
+++ b/app/assets/javascripts/nav/components/new_nav_toggle.vue
@@ -51,7 +51,7 @@ export default {
Tracking.event(undefined, 'click_toggle', {
label: this.enabled ? 'disable_new_nav_beta' : 'enable_new_nav_beta',
- property: this.enabled ? 'navigation' : 'navigation_top',
+ property: this.enabled ? 'nav_user_menu' : 'navigation_top',
});
window.location.reload();
diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue
index f68587f610b..d8452900e34 100644
--- a/app/assets/javascripts/super_sidebar/components/user_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue
@@ -11,6 +11,7 @@ import { s__, __, sprintf } from '~/locale';
import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import Tracking from '~/tracking';
import PersistentUserCallout from '~/persistent_user_callout';
+import { USER_MENU_TRACKING_DEFAULTS } from '../constants';
import UserNameGroup from './user_name_group.vue';
export default {
@@ -72,6 +73,10 @@ export default {
return {
text: this.$options.i18n.startTrial,
href: this.data.trial.url,
+ extraAttrs: {
+ ...USER_MENU_TRACKING_DEFAULTS,
+ 'data-track-label': 'start_trial',
+ },
};
},
editProfileItem() {
@@ -80,6 +85,8 @@ export default {
href: this.data.settings.profile_path,
extraAttrs: {
'data-qa-selector': 'edit_profile_link',
+ ...USER_MENU_TRACKING_DEFAULTS,
+ 'data-track-label': 'user_edit_profile',
},
};
},
@@ -87,6 +94,10 @@ export default {
return {
text: this.$options.i18n.preferences,
href: this.data.settings.profile_preferences_path,
+ extraAttrs: {
+ ...USER_MENU_TRACKING_DEFAULTS,
+ 'data-track-label': 'user_preferences',
+ },
};
},
addBuyPipelineMinutesMenuItem() {
@@ -99,6 +110,8 @@ export default {
href: this.data.pipeline_minutes?.buy_pipeline_minutes_path,
extraAttrs: {
class: 'js-follow-link',
+ ...USER_MENU_TRACKING_DEFAULTS,
+ 'data-track-label': 'buy_pipeline_minutes',
},
};
},
@@ -106,6 +119,10 @@ export default {
return {
text: this.$options.i18n.gitlabNext,
href: this.data.canary_toggle_com_url,
+ extraAttrs: {
+ ...USER_MENU_TRACKING_DEFAULTS,
+ 'data-track-label': 'switch_to_canary',
+ },
};
},
feedbackItem() {
@@ -114,6 +131,8 @@ export default {
href: this.$options.feedbackUrl,
extraAttrs: {
target: '_blank',
+ ...USER_MENU_TRACKING_DEFAULTS,
+ 'data-track-label': 'provide_nav_beta_feedback',
},
};
},
@@ -167,18 +186,20 @@ export default {
},
methods: {
onShow() {
- this.trackEvents();
- this.initCallout();
+ this.initBuyCIMinsCallout();
},
closeDropdown() {
this.$refs.userDropdown.close();
},
- initCallout() {
+ initBuyCIMinsCallout() {
if (this.showNotificationDot) {
PersistentUserCallout.factory(this.$refs?.buyPipelineMinutesNotificationCallout.$el);
}
},
- trackEvents() {
+ /* We're not sure this event is tracked by anyone
+ whether it stays will depend on the outcome of this discussion:
+ https://gitlab.com/gitlab-org/gitlab/-/issues/402713#note_1343072135 */
+ trackBuyCIMins() {
if (this.addBuyPipelineMinutesMenuItem) {
const {
'track-action': trackAction,
@@ -188,6 +209,12 @@ export default {
this.track(trackAction, { label, property });
}
},
+ trackSignOut() {
+ this.track(USER_MENU_TRACKING_DEFAULTS['data-track-action'], {
+ label: 'user_sign_out',
+ property: USER_MENU_TRACKING_DEFAULTS['data-track-property'],
+ });
+ },
},
};
</script>
@@ -251,6 +278,7 @@ export default {
:item="buyPipelineMinutesItem"
v-bind="buyPipelineMinutesCalloutData"
data-testid="buy-pipeline-minutes-item"
+ @action="trackBuyCIMins"
>
<template #list-item>
<span class="gl-display-flex gl-flex-direction-column">
@@ -287,6 +315,7 @@ export default {
bordered
:group="signOutGroup"
data-testid="sign-out-group"
+ @action="trackSignOut"
/>
</gl-disclosure-dropdown>
diff --git a/app/assets/javascripts/super_sidebar/components/user_name_group.vue b/app/assets/javascripts/super_sidebar/components/user_name_group.vue
index 2489f462122..a24299d449b 100644
--- a/app/assets/javascripts/super_sidebar/components/user_name_group.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_name_group.vue
@@ -1,8 +1,8 @@
<script>
import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlTooltip } from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
-
import { s__ } from '~/locale';
+import { USER_MENU_TRACKING_DEFAULTS } from '../constants';
export default {
i18n: {
@@ -31,7 +31,13 @@ export default {
};
if (this.user.has_link_to_profile) {
item.href = this.user.link_to_profile;
+
+ item.extraAttrs = {
+ ...USER_MENU_TRACKING_DEFAULTS,
+ 'data-track-label': 'user_profile',
+ };
}
+
return item;
},
},
diff --git a/app/assets/javascripts/super_sidebar/constants.js b/app/assets/javascripts/super_sidebar/constants.js
index 82955341e9c..227619d3277 100644
--- a/app/assets/javascripts/super_sidebar/constants.js
+++ b/app/assets/javascripts/super_sidebar/constants.js
@@ -20,3 +20,8 @@ export const TRACKING_UNKNOWN_PANEL = 'nav_panel_unknown';
export const CLICK_MENU_ITEM_ACTION = 'click_menu_item';
export const PANELS_WITH_PINS = ['group', 'project'];
+
+export const USER_MENU_TRACKING_DEFAULTS = {
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+};
diff --git a/doc/ci/pipelines/cicd_minutes.md b/doc/ci/pipelines/cicd_minutes.md
index 8ea42206adb..2528a53088d 100644
--- a/doc/ci/pipelines/cicd_minutes.md
+++ b/doc/ci/pipelines/cicd_minutes.md
@@ -317,6 +317,13 @@ On GitLab SaaS an email notification is sent to the namespace owners when:
- The available CI/CD minutes are below 5% of the quota.
- All CI/CD minutes have been used.
+### Special quota limits
+
+In some cases, the quota limit is replaced by one of the following labels:
+
+- **Unlimited minutes**: For namespaces with unlimited CI/CD minutes
+- **Not supported minutes**: For namespaces where active shared runners are not enabled
+
## Reduce consumption of CI/CD minutes
If your project consumes too many CI/CD minutes, there are some strategies you can
diff --git a/doc/ci/pipelines/merge_request_pipelines.md b/doc/ci/pipelines/merge_request_pipelines.md
index 881e94a6559..d42f35627a2 100644
--- a/doc/ci/pipelines/merge_request_pipelines.md
+++ b/doc/ci/pipelines/merge_request_pipelines.md
@@ -202,16 +202,20 @@ It's possible to have both branch pipelines and merge request pipelines in the
**Pipelines** tab of a single merge request. This might be [by configuration](../yaml/workflow.md#switch-between-branch-pipelines-and-merge-request-pipelines),
or [by accident](#two-pipelines-when-pushing-to-a-branch).
-If both types of pipelines are in one merge request, the merge request's pipeline
-is not considered successful if:
-
-- The branch pipeline succeeds.
-- The merge request pipeline fails.
-
When using the [merge when pipeline succeeds](../../user/project/merge_requests/merge_when_pipeline_succeeds.md)
feature and both pipelines types are present, the merge request pipelines are checked,
not the branch pipelines.
+Therefore, the MR pipeline result is marked as unsuccessful if the
+**merge request pipeline** fails, independently of the **branch pipeline** result.
+
+However:
+
+- These conditions are not enforced.
+- A race condition determines which pipeline's result is used to either block or pass merge requests.
+
+This bug is tracked on [issue 384927](https://gitlab.com/gitlab-org/gitlab/-/issues/384927).
+
### `An error occurred while trying to run a new pipeline for this merge request.`
This error can happen when you select **Run pipeline** in a merge request, but the
diff --git a/doc/integration/jira/configure.md b/doc/integration/jira/configure.md
index c825b584b57..2533ce2b643 100644
--- a/doc/integration/jira/configure.md
+++ b/doc/integration/jira/configure.md
@@ -59,6 +59,28 @@ To configure your project:
Your GitLab project can now interact with all Jira projects in your instance and the project now
displays a Jira link that opens the Jira project.
+## Create a Jira Cloud API token
+
+To [integrate with Jira](index.md) in Atlassian Cloud, you must create an API token.
+
+To create a Jira Cloud API token:
+
+1. Sign in to [Atlassian](https://id.atlassian.com/manage-profile/security/api-tokens)
+ from an account with **write** access to Jira projects.
+
+ The link opens the **API tokens** page. Alternatively, from your Atlassian
+ profile, select **Account Settings > Security > Create and manage API tokens**.
+
+1. Select **Create API token**.
+1. In the dialog, enter a label for your token and select **Create**.
+
+To copy the API token, select **Copy** and paste the token somewhere safe.
+
+To configure GitLab, you need:
+
+- The newly created token
+- The email address you used when you created the token
+
## Migrate from Jira Server to Jira Cloud in GitLab
To migrate from Jira Server to Jira Cloud in GitLab and maintain your Jira integration:
@@ -68,7 +90,7 @@ To migrate from Jira Server to Jira Cloud in GitLab and maintain your Jira integ
1. Select **Jira**.
1. In **Web URL**, enter the new Jira site URL (for example, `https://myjirasite.atlassian.net`).
1. In **Username or Email**, enter the username or email registered on your Jira profile.
-1. [Create an API token](jira_cloud_configuration.md), and copy that value.
+1. [Create an API token](#create-a-jira-cloud-api-token), and copy that value.
1. In **Password or API token**, paste the API token value.
1. Optional. Select **Test settings** to check if the connection is working.
1. Select **Save changes**.
diff --git a/doc/integration/jira/index.md b/doc/integration/jira/index.md
index 3ec4c7aee87..41d6cda4c84 100644
--- a/doc/integration/jira/index.md
+++ b/doc/integration/jira/index.md
@@ -64,7 +64,7 @@ The authentication method in Jira depends on whether you host Jira on your own s
how to [set up a user in Jira Server](jira_server_configuration.md).
- **Jira on Atlassian cloud** supports authentication through an API token. When connecting to Jira on
Atlassian cloud, an email and API token are required. For more information, see
- [Create a Jira Cloud API token](jira_cloud_configuration.md).
+ [Create a Jira Cloud API token](configure.md#create-a-jira-cloud-api-token).
## Privacy considerations
diff --git a/doc/integration/jira/jira_cloud_configuration.md b/doc/integration/jira/jira_cloud_configuration.md
index 9d52368f528..83321df0420 100644
--- a/doc/integration/jira/jira_cloud_configuration.md
+++ b/doc/integration/jira/jira_cloud_configuration.md
@@ -1,23 +1,11 @@
---
-stage: Manage
-group: Integrations
-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: 'configure.md#create-a-jira-cloud-api-token'
+remove_date: '2023-07-07'
---
-# Create a Jira Cloud API token **(FREE)**
+This document was moved to [another location](configure.md#create-a-jira-cloud-api-token).
-You need an API token to [integrate with Jira](index.md)
-in Atlassian Cloud. To create the API token:
-
-1. Sign in to [Atlassian](https://id.atlassian.com/manage-profile/security/api-tokens)
- using an account with *write* access to Jira projects.
-
- The link opens the API tokens page. Alternatively, to go to this page from your Atlassian
- profile, select **Account Settings > Security > Create and manage API tokens**.
-1. Select **Create API token**.
-1. In the dialog, enter a label for your token and select **Create**.
-1. To copy the API token, select **Copy**, then paste the token somewhere safe.
-
-You need the newly created token, and the email
-address you used when you created it, when you
-[configure GitLab](configure.md).
+<!-- This redirect file can be deleted after <2023-07-07>. -->
+<!-- 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/user/permissions.md b/doc/user/permissions.md
index 928e2dbe6a2..4ca10caef86 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -23,6 +23,7 @@ The available roles are:
- Developer
- Maintainer
- Owner
+- Minimal Access (available for the top-level group only)
A user assigned the Guest role has the least permissions,
and the Owner has the most.
@@ -423,28 +424,28 @@ nested groups if you have membership in one of its parents.
For more information, see
[subgroup memberships](group/subgroups/index.md#subgroup-membership).
-## Users with minimal access **(PREMIUM)**
+## Users with Minimal Access **(PREMIUM)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40942) in GitLab 13.4.
-> - Support for inviting users with minimal access role [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106438) in GitLab 15.9.
+> - Support for inviting users with Minimal Access role [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106438) in GitLab 15.9.
-Owners can add members with a "minimal access" role to a root group. Such users do not:
+Users with the Minimal Access role:
- Count as licensed seats on self-managed Ultimate subscriptions or any GitLab.com subscriptions.
- Automatically have access to projects and subgroups in that root group.
-Owners must explicitly add these "minimal access" users to the specific subgroups and
+Owners must explicitly add these users to the specific subgroups and
projects.
-You can use minimal access to give the same member more than one role in a group:
+You can use the Minimal Access role to give the same member more than one role in a group:
-1. Add the member to the root group with a minimal access role.
+1. Add the member to the root group with a Minimal Access role.
1. Invite the member as a direct member with a specific role in any subgroup or project in that group.
-Because of an [outstanding issue](https://gitlab.com/gitlab-org/gitlab/-/issues/267996), when minimal access users:
+Because of an [outstanding issue](https://gitlab.com/gitlab-org/gitlab/-/issues/267996), when a user with the Minimal Access role:
-- Sign in with standard web authentication, they receive a `404` error when accessing the parent group.
-- Sign in with Group SSO, they receive a `404` error immediately because they are redirected to the parent group page.
+- Signs in with standard web authentication, they receive a `404` error when accessing the parent group.
+- Signs in with Group SSO, they receive a `404` error immediately because they are redirected to the parent group page.
To work around the issue, give these users the Guest role or higher to any project or subgroup within the parent group.
@@ -538,8 +539,4 @@ the Owner role:
### Known issues
- Additional permissions can only be applied to users with the Guest role.
-- There is no visual distinction in the UI between the Guest role and the Guest role with additional permission. For more information, see [issue 384099](https://gitlab.com/gitlab-org/gitlab/-/issues/384099).
- If a user with a custom role is shared with a group or project, their custom role is not transferred over with them. The user has the regular Guest role in the new group or project.
-- If a custom role is deleted, the users associated with that custom role are also removed from the group. For more information, see [issue 370352](https://gitlab.com/gitlab-org/gitlab/-/issues/370352).
-- The API endpoint for associating a custom role with a user only works for users with the Guest role in a group. A project member can be associated with a custom role, but not through the API yet. For more information, see [issue 385495](https://gitlab.com/gitlab-org/gitlab/-/issues/385495).
-- The only way to remove a custom role from a user's membership to a Group is to delete the custom role, which deletes the user membership entirely. See [issue 387769](https://gitlab.com/gitlab-org/gitlab/-/issues/387769).
diff --git a/lib/gitlab/background_migration/backfill_partitioned_table.rb b/lib/gitlab/background_migration/backfill_partitioned_table.rb
new file mode 100644
index 00000000000..6479d40a930
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_partitioned_table.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Background migration to generically copy data from the given table into its corresponding partitioned table
+ class BackfillPartitionedTable < BatchedMigrationJob
+ operation_name :upsert_partitioned_table
+ feature_category :database
+ job_arguments :partitioned_table
+
+ def perform
+ validate_paritition_table!
+
+ bulk_copy = Gitlab::Database::PartitioningMigrationHelpers::BulkCopy.new(
+ batch_table,
+ partitioned_table,
+ batch_column,
+ connection: connection
+ )
+
+ each_sub_batch do |relation|
+ sub_start_id, sub_stop_id = relation.pick(Arel.sql("MIN(#{batch_column}), MAX(#{batch_column})"))
+ bulk_copy.copy_between(sub_start_id, sub_stop_id)
+ end
+ end
+
+ private
+
+ def validate_paritition_table!
+ unless connection.table_exists?(partitioned_table)
+ raise "exiting backfill migration because partitioned table #{partitioned_table} does not exist. " \
+ "This could be due to rollback of the migration which created the partitioned table."
+ end
+
+ # rubocop: disable Style/GuardClause
+ unless Gitlab::Database::PostgresPartitionedTable.find_by_name_in_current_schema(partitioned_table).present?
+ raise "exiting backfill migration because the given destination table is not partitioned."
+ end
+ # rubocop: enable Style/GuardClause
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb b/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb
index dcf457b9d63..e87707953ae 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb
@@ -21,7 +21,7 @@ module Gitlab
return
end
- bulk_copy = BulkCopy.new(source_table, partitioned_table, source_column, connection: connection)
+ bulk_copy = Gitlab::Database::PartitioningMigrationHelpers::BulkCopy.new(source_table, partitioned_table, source_column, connection: connection)
parent_batch_relation = relation_scoped_to_range(source_table, source_column, start_id, stop_id)
parent_batch_relation.each_batch(of: SUB_BATCH_SIZE) do |sub_batch|
@@ -56,41 +56,6 @@ module Gitlab
def mark_jobs_as_succeeded(*arguments)
BackgroundMigrationJob.mark_all_as_succeeded(self.class.name, arguments)
end
-
- # Helper class to copy data between two tables via upserts
- class BulkCopy
- DELIMITER = ', '
-
- attr_reader :source_table, :destination_table, :source_column, :connection
-
- def initialize(source_table, destination_table, source_column, connection:)
- @source_table = source_table
- @destination_table = destination_table
- @source_column = source_column
- @connection = connection
- end
-
- def copy_between(start_id, stop_id)
- connection.execute(<<~SQL)
- INSERT INTO #{destination_table} (#{column_listing})
- SELECT #{column_listing}
- FROM #{source_table}
- WHERE #{source_column} BETWEEN #{start_id} AND #{stop_id}
- FOR UPDATE
- ON CONFLICT (#{conflict_targets}) DO NOTHING
- SQL
- end
-
- private
-
- def column_listing
- @column_listing ||= connection.columns(source_table).map(&:name).join(DELIMITER)
- end
-
- def conflict_targets
- connection.primary_key(destination_table).join(DELIMITER)
- end
- end
end
end
end
diff --git a/lib/gitlab/database/partitioning_migration_helpers/bulk_copy.rb b/lib/gitlab/database/partitioning_migration_helpers/bulk_copy.rb
new file mode 100644
index 00000000000..b8f5a2e3ad4
--- /dev/null
+++ b/lib/gitlab/database/partitioning_migration_helpers/bulk_copy.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module PartitioningMigrationHelpers
+ # Helper class to copy data between two tables via upserts
+ class BulkCopy
+ DELIMITER = ', '
+
+ attr_reader :source_table, :destination_table, :source_column, :connection
+
+ def initialize(source_table, destination_table, source_column, connection:)
+ @source_table = source_table
+ @destination_table = destination_table
+ @source_column = source_column
+ @connection = connection
+ end
+
+ def copy_between(start_id, stop_id)
+ connection.execute(<<~SQL)
+ INSERT INTO #{destination_table} (#{column_listing})
+ SELECT #{column_listing}
+ FROM #{source_table}
+ WHERE #{source_column} BETWEEN #{start_id} AND #{stop_id}
+ FOR UPDATE
+ ON CONFLICT (#{conflict_targets}) DO NOTHING
+ SQL
+ end
+
+ private
+
+ def column_listing
+ @column_listing ||= connection.columns(source_table).map(&:name).join(DELIMITER)
+ end
+
+ def conflict_targets
+ connection.primary_keys(destination_table).join(DELIMITER)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
index 5a942577006..e3cf1298df6 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
@@ -12,8 +12,10 @@ module Gitlab
ERROR_SCOPE = 'table partitioning'
MIGRATION_CLASS_NAME = "::#{module_parent_name}::BackfillPartitionedTable"
+ MIGRATION = "BackfillPartitionedTable"
BATCH_INTERVAL = 2.minutes.freeze
BATCH_SIZE = 50_000
+ SUB_BATCH_SIZE = 2_500
JobArguments = Struct.new(:start_id, :stop_id, :source_table_name, :partitioned_table_name, :source_column) do
def self.from_array(arguments)
@@ -108,7 +110,16 @@ module Gitlab
partitioned_table_name = make_partitioned_table_name(table_name)
primary_key = connection.primary_key(table_name)
- enqueue_background_migration(table_name, partitioned_table_name, primary_key)
+
+ queue_batched_background_migration(
+ MIGRATION,
+ table_name,
+ primary_key,
+ partitioned_table_name,
+ batch_size: BATCH_SIZE,
+ sub_batch_size: SUB_BATCH_SIZE,
+ job_interval: BATCH_INTERVAL
+ )
end
# Cleanup a previously enqueued background migration to copy data into a partitioned table. This will not
@@ -150,7 +161,7 @@ module Gitlab
# 2. Inline copy any missed rows from the original table to the partitioned table
#
# **NOTE** Migrations using this method cannot be scheduled in the same release as the migration that
- # schedules the background migration using the `enqueue_background_migration` helper, or else the
+ # schedules the background migration using the `enqueue_partitioning_data_migration` helper, or else the
# background migration jobs will be force-executed.
#
# Example:
@@ -445,18 +456,6 @@ module Gitlab
create_trigger(table_name, trigger_name, function_name, fires: 'AFTER INSERT OR UPDATE OR DELETE')
end
- def enqueue_background_migration(source_table_name, partitioned_table_name, source_column)
- source_model = define_batchable_model(source_table_name)
-
- queue_background_migration_jobs_by_range_at_intervals(
- source_model,
- MIGRATION_CLASS_NAME,
- BATCH_INTERVAL,
- batch_size: BATCH_SIZE,
- other_job_arguments: [source_table_name.to_s, partitioned_table_name, source_column],
- track_jobs: true)
- end
-
def cleanup_migration_jobs(table_name)
::Gitlab::Database::BackgroundMigrationJob.for_partitioning_migration(MIGRATION_CLASS_NAME, table_name).delete_all
end
diff --git a/lib/gitlab/legacy_github_import/user_formatter.rb b/lib/gitlab/legacy_github_import/user_formatter.rb
index d45a166d2b7..8fd8354e59c 100644
--- a/lib/gitlab/legacy_github_import/user_formatter.rb
+++ b/lib/gitlab/legacy_github_import/user_formatter.rb
@@ -5,6 +5,8 @@ module Gitlab
class UserFormatter
attr_reader :client, :raw
+ GITEA_GHOST_EMAIL = 'ghost_user@gitea_import_dummy_email.com'
+
def initialize(client, raw)
@client = client
@raw = raw
@@ -27,7 +29,14 @@ module Gitlab
private
def email
- @email ||= client.user(raw[:login]).to_h[:email]
+ # Gitea marks deleted users as 'Ghost' users and removes them from
+ # their system. So for Gitea 'Ghost' users we need to assign a dummy
+ # email address to avoid querying the Gitea api for a non existing user
+ if raw[:login] == 'Ghost' && raw[:id] == -1
+ @email = GITEA_GHOST_EMAIL
+ else
+ @email ||= client.user(raw[:login]).to_h[:email]
+ end
end
def find_by_email
diff --git a/spec/frontend/nav/components/new_nav_toggle_spec.js b/spec/frontend/nav/components/new_nav_toggle_spec.js
index fe543a346b5..cf8e59d6522 100644
--- a/spec/frontend/nav/components/new_nav_toggle_spec.js
+++ b/spec/frontend/nav/components/new_nav_toggle_spec.js
@@ -9,6 +9,7 @@ import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import { s__ } from '~/locale';
+import { mockTracking } from 'helpers/tracking_helper';
jest.mock('~/alert');
@@ -18,6 +19,7 @@ describe('NewNavToggle', () => {
useMockLocationHelper();
let wrapper;
+ let trackingSpy;
const findToggle = () => wrapper.findComponent(GlToggle);
const findDisclosureItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
@@ -29,6 +31,8 @@ describe('NewNavToggle', () => {
...propsData,
},
});
+
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
};
const getByText = (text, options) =>
@@ -61,15 +65,17 @@ describe('NewNavToggle', () => {
});
describe.each`
- desc | actFn
- ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')}
- ${'on menu item action'} | ${() => findDisclosureItem().vm.$emit('action')}
- `('$desc', ({ actFn }) => {
+ desc | actFn | toggleValue | trackingLabel | trackingProperty
+ ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')} | ${false} | ${'enable_new_nav_beta'} | ${'navigation_top'}
+ ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')} | ${false} | ${'enable_new_nav_beta'} | ${'navigation_top'}
+ ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')} | ${true} | ${'disable_new_nav_beta'} | ${'nav_user_menu'}
+ ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')} | ${true} | ${'disable_new_nav_beta'} | ${'nav_user_menu'}
+ `('$desc', ({ actFn, toggleValue, trackingLabel, trackingProperty }) => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
- createComponent({ enabled: false, newNavigation: true });
+ createComponent({ enabled: toggleValue });
});
it('reloads the page on success', async () => {
@@ -100,7 +106,17 @@ describe('NewNavToggle', () => {
it('changes the toggle', async () => {
await actFn();
- expect(findToggle().props('value')).toBe(true);
+ expect(findToggle().props('value')).toBe(!toggleValue);
+ });
+
+ it('tracks the Snowplow event', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK);
+ await actFn();
+ await waitForPromises();
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', {
+ label: trackingLabel,
+ property: trackingProperty,
+ });
});
afterEach(() => {
@@ -136,15 +152,17 @@ describe('NewNavToggle', () => {
});
describe.each`
- desc | actFn
- ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')}
- ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')}
- `('$desc', ({ actFn }) => {
+ desc | actFn | toggleValue | trackingLabel | trackingProperty
+ ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')} | ${false} | ${'enable_new_nav_beta'} | ${'navigation_top'}
+ ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')} | ${false} | ${'enable_new_nav_beta'} | ${'navigation_top'}
+ ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')} | ${true} | ${'disable_new_nav_beta'} | ${'nav_user_menu'}
+ ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')} | ${true} | ${'disable_new_nav_beta'} | ${'nav_user_menu'}
+ `('$desc', ({ actFn, toggleValue, trackingLabel, trackingProperty }) => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
- createComponent({ enabled: false });
+ createComponent({ enabled: toggleValue });
});
it('reloads the page on success', async () => {
@@ -175,7 +193,17 @@ describe('NewNavToggle', () => {
it('changes the toggle', async () => {
await actFn();
- expect(findToggle().props('value')).toBe(true);
+ expect(findToggle().props('value')).toBe(!toggleValue);
+ });
+
+ it('tracks the Snowplow event', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK);
+ await actFn();
+ await waitForPromises();
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', {
+ label: trackingLabel,
+ property: trackingProperty,
+ });
});
afterEach(() => {
diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js
index 381cfdd8c81..25bcd322d32 100644
--- a/spec/frontend/super_sidebar/components/user_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/user_menu_spec.js
@@ -171,7 +171,7 @@ describe('UserMenu component', () => {
let item;
const setItem = ({ has_start_trial } = {}) => {
- createWrapper({ trial: { has_start_trial } });
+ createWrapper({ trial: { has_start_trial, url: '' } });
item = wrapper.findByTestId('start-trial-item');
};
@@ -188,6 +188,15 @@ describe('UserMenu component', () => {
expect(item.exists()).toBe(true);
});
});
+
+ it('has Snowplow tracking attributes', () => {
+ setItem({ has_start_trial: true });
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'start_trial',
+ });
+ });
});
describe('Buy Pipeline Minutes item', () => {
@@ -230,17 +239,30 @@ describe('UserMenu component', () => {
expect(item.exists()).toBe(true);
});
- it('tracks the Sentry event', () => {
- setItem({ show_buy_pipeline_minutes: true });
- showDropdown();
- expect(trackingSpy).toHaveBeenCalledWith(
- undefined,
- userMenuMockPipelineMinutes.tracking_attrs['track-action'],
- {
- label: userMenuMockPipelineMinutes.tracking_attrs['track-label'],
- property: userMenuMockPipelineMinutes.tracking_attrs['track-property'],
- },
- );
+ describe('Snowplow tracking attributes to track item click', () => {
+ beforeEach(() => {
+ setItem({ show_buy_pipeline_minutes: true });
+ });
+
+ it('has attributes to track item click in scope of new nav', () => {
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'buy_pipeline_minutes',
+ });
+ });
+
+ it('tracks the click on the item', () => {
+ item.vm.$emit('action');
+ expect(trackingSpy).toHaveBeenCalledWith(
+ undefined,
+ userMenuMockPipelineMinutes.tracking_attrs['track-action'],
+ {
+ label: userMenuMockPipelineMinutes.tracking_attrs['track-label'],
+ property: userMenuMockPipelineMinutes.tracking_attrs['track-property'],
+ },
+ );
+ });
});
describe('Callout & notification dot', () => {
@@ -320,33 +342,71 @@ describe('UserMenu component', () => {
});
describe('Edit profile item', () => {
- it('should render a link to the profile page', () => {
+ let item;
+
+ beforeEach(() => {
createWrapper();
- const item = wrapper.findByTestId('edit-profile-item');
+ item = wrapper.findByTestId('edit-profile-item');
+ });
+
+ it('should render a link to the profile page', () => {
expect(item.text()).toBe(UserMenu.i18n.editProfile);
expect(item.find('a').attributes('href')).toBe(userMenuMockData.settings.profile_path);
});
+
+ it('has Snowplow tracking attributes', () => {
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'user_edit_profile',
+ });
+ });
});
describe('Preferences item', () => {
- it('should render a link to the profile page', () => {
+ let item;
+
+ beforeEach(() => {
createWrapper();
- const item = wrapper.findByTestId('preferences-item');
+ item = wrapper.findByTestId('preferences-item');
+ });
+
+ it('should render a link to the profile page', () => {
expect(item.text()).toBe(UserMenu.i18n.preferences);
expect(item.find('a').attributes('href')).toBe(
userMenuMockData.settings.profile_preferences_path,
);
});
+
+ it('has Snowplow tracking attributes', () => {
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'user_preferences',
+ });
+ });
});
describe('GitLab Next item', () => {
describe('on gitlab.com', () => {
- it('should render a link to switch to GitLab Next', () => {
+ let item;
+
+ beforeEach(() => {
createWrapper({ gitlab_com_but_not_canary: true });
- const item = wrapper.findByTestId('gitlab-next-item');
+ item = wrapper.findByTestId('gitlab-next-item');
+ });
+ it('should render a link to switch to GitLab Next', () => {
expect(item.text()).toBe(UserMenu.i18n.gitlabNext);
expect(item.find('a').attributes('href')).toBe(userMenuMockData.canary_toggle_com_url);
});
+
+ it('has Snowplow tracking attributes', () => {
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'switch_to_canary',
+ });
+ });
});
describe('anywhere else', () => {
@@ -368,10 +428,23 @@ describe('UserMenu component', () => {
});
describe('Feedback item', () => {
- it('should render feedback item with a link to a new GitLab issue', () => {
+ let item;
+
+ beforeEach(() => {
createWrapper();
- const feedbackItem = wrapper.findByTestId('feedback-item');
- expect(feedbackItem.find('a').attributes('href')).toBe(UserMenu.feedbackUrl);
+ item = wrapper.findByTestId('feedback-item');
+ });
+
+ it('should render feedback item with a link to a new GitLab issue', () => {
+ expect(item.find('a').attributes('href')).toBe(UserMenu.feedbackUrl);
+ });
+
+ it('has Snowplow tracking attributes', () => {
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'provide_nav_beta_feedback',
+ });
});
});
@@ -398,6 +471,15 @@ describe('UserMenu component', () => {
);
expect(findSignOutGroup().find('a').attributes('data-method')).toBe('post');
});
+
+ it('should track Snowplow event on sign out', () => {
+ findSignOutGroup().vm.$emit('action');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
+ label: 'user_sign_out',
+ property: 'nav_user_menu',
+ });
+ });
});
});
});
diff --git a/spec/frontend/super_sidebar/components/user_name_group_spec.js b/spec/frontend/super_sidebar/components/user_name_group_spec.js
index c06c8c218d4..6e3b18d3107 100644
--- a/spec/frontend/super_sidebar/components/user_name_group_spec.js
+++ b/spec/frontend/super_sidebar/components/user_name_group_spec.js
@@ -41,10 +41,12 @@ describe('UserNameGroup component', () => {
});
it('passes the item to the disclosure dropdown item', () => {
- expect(findGlDisclosureDropdownItem().props('item')).toEqual({
- text: userMenuMockData.name,
- href: userMenuMockData.link_to_profile,
- });
+ expect(findGlDisclosureDropdownItem().props('item')).toEqual(
+ expect.objectContaining({
+ text: userMenuMockData.name,
+ href: userMenuMockData.link_to_profile,
+ }),
+ );
});
it("renders user's name", () => {
@@ -97,4 +99,16 @@ describe('UserNameGroup component', () => {
});
});
});
+
+ describe('Tracking', () => {
+ it('sets the tracking attributes', () => {
+ expect(findGlDisclosureDropdownItem().find('a').attributes()).toEqual(
+ expect.objectContaining({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'user_profile',
+ }),
+ );
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
index 1d8f595a400..990a4110d4b 100644
--- a/spec/frontend/super_sidebar/mock_data.js
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -140,6 +140,7 @@ export const userMenuMockData = {
status: userMenuMockStatus,
trial: {
has_start_trial: false,
+ url: invalidUrl,
},
settings: {
profile_path: invalidUrl,
diff --git a/spec/lib/gitlab/background_migration/backfill_partitioned_table_spec.rb b/spec/lib/gitlab/background_migration/backfill_partitioned_table_spec.rb
new file mode 100644
index 00000000000..53216cc780b
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_partitioned_table_spec.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillPartitionedTable, feature_category: :database do
+ subject(:backfill_job) do
+ described_class.new(
+ start_id: 1,
+ end_id: 3,
+ batch_table: source_table,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ job_arguments: [destination_table],
+ connection: connection
+ )
+ end
+
+ let(:connection) { ApplicationRecord.connection }
+ let(:source_table) { '_test_source_table' }
+ let(:destination_table) { "#{source_table}_partitioned" }
+ let(:source_model) { Class.new(ApplicationRecord) }
+ let(:destination_model) { Class.new(ApplicationRecord) }
+
+ describe '#perform' do
+ context 'without the destination table' do
+ let(:expected_error_message) do
+ "exiting backfill migration because partitioned table #{destination_table} does not exist. " \
+ "This could be due to rollback of the migration which created the partitioned table."
+ end
+
+ it 'raises an exception' do
+ expect { backfill_job.perform }.to raise_error(expected_error_message)
+ end
+ end
+
+ context 'with destination table being not partitioned' do
+ before do
+ connection.execute(<<~SQL)
+ CREATE TABLE #{destination_table} (
+ id serial NOT NULL,
+ col1 int NOT NULL,
+ col2 text NOT NULL,
+ created_at timestamptz NOT NULL,
+ PRIMARY KEY (id, created_at)
+ )
+ SQL
+ end
+
+ after do
+ connection.drop_table destination_table
+ end
+
+ let(:expected_error_message) do
+ "exiting backfill migration because the given destination table is not partitioned."
+ end
+
+ it 'raises an exception' do
+ expect { backfill_job.perform }.to raise_error(expected_error_message)
+ end
+ end
+
+ context 'when the destination table exists' do
+ before do
+ connection.execute(<<~SQL)
+ CREATE TABLE #{source_table} (
+ id serial NOT NULL PRIMARY KEY,
+ col1 int NOT NULL,
+ col2 text NOT NULL,
+ created_at timestamptz NOT NULL
+ )
+ SQL
+
+ connection.execute(<<~SQL)
+ CREATE TABLE #{destination_table} (
+ id serial NOT NULL,
+ col1 int NOT NULL,
+ col2 text NOT NULL,
+ created_at timestamptz NOT NULL,
+ PRIMARY KEY (id, created_at)
+ ) PARTITION BY RANGE (created_at)
+ SQL
+
+ connection.execute(<<~SQL)
+ CREATE TABLE #{destination_table}_202001 PARTITION OF #{destination_table}
+ FOR VALUES FROM ('2020-01-01') TO ('2020-02-01')
+ SQL
+
+ connection.execute(<<~SQL)
+ CREATE TABLE #{destination_table}_202002 PARTITION OF #{destination_table}
+ FOR VALUES FROM ('2020-02-01') TO ('2020-03-01')
+ SQL
+
+ source_model.table_name = source_table
+ destination_model.table_name = destination_table
+ end
+
+ after do
+ connection.drop_table source_table
+ connection.drop_table destination_table
+ end
+
+ let(:timestamp) { Time.utc(2020, 1, 2).round }
+ let!(:source1) { create_source_record(timestamp) }
+ let!(:source2) { create_source_record(timestamp + 1.day) }
+ let!(:source3) { create_source_record(timestamp + 1.month) }
+
+ it 'copies data into the destination table idempotently' do
+ expect(destination_model.count).to eq(0)
+
+ backfill_job.perform
+
+ expect(destination_model.count).to eq(3)
+
+ source_model.find_each do |source_record|
+ destination_record = destination_model.find_by_id(source_record.id)
+
+ expect(destination_record.attributes).to eq(source_record.attributes)
+ end
+
+ backfill_job.perform
+
+ expect(destination_model.count).to eq(3)
+ end
+
+ it 'breaks the assigned batch into smaller sub batches' do
+ expect_next_instance_of(Gitlab::Database::PartitioningMigrationHelpers::BulkCopy) do |bulk_copy|
+ expect(bulk_copy).to receive(:copy_between).with(source1.id, source2.id)
+ expect(bulk_copy).to receive(:copy_between).with(source3.id, source3.id)
+ end
+
+ backfill_job.perform
+ end
+ end
+ end
+
+ def create_source_record(timestamp)
+ source_model.create!(col1: 123, col2: 'original value', created_at: timestamp)
+ end
+end
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb
index 1885e84ac4c..fc279051800 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb
@@ -54,6 +54,11 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartition
allow(backfill_job).to receive(:sleep)
end
+ after do
+ connection.drop_table source_table
+ connection.drop_table destination_table
+ end
+
let(:source_model) { Class.new(ActiveRecord::Base) }
let(:destination_model) { Class.new(ActiveRecord::Base) }
let(:timestamp) { Time.utc(2020, 1, 2).round }
@@ -82,7 +87,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartition
end
it 'breaks the assigned batch into smaller batches' do
- expect_next_instance_of(described_class::BulkCopy) do |bulk_copy|
+ expect_next_instance_of(Gitlab::Database::PartitioningMigrationHelpers::BulkCopy) do |bulk_copy|
expect(bulk_copy).to receive(:copy_between).with(source1.id, source2.id)
expect(bulk_copy).to receive(:copy_between).with(source3.id, source3.id)
end
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
index 06c40f18b43..d87ef7a0953 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
include Database::PartitioningHelpers
include Database::TriggerHelpers
include Database::TableSchemaHelpers
+ include MigrationsHelpers
let(:migration) do
ActiveRecord::Migration.new.extend(described_class)
@@ -485,17 +486,15 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
end
context 'when records exist in the source table' do
- let(:migration_class) { '::Gitlab::Database::PartitioningMigrationHelpers::BackfillPartitionedTable' }
+ let(:migration_class) { described_class::MIGRATION }
let(:sub_batch_size) { described_class::SUB_BATCH_SIZE }
- let(:pause_seconds) { described_class::PAUSE_SECONDS }
let!(:first_id) { source_model.create!(name: 'Bob', age: 20).id }
let!(:second_id) { source_model.create!(name: 'Alice', age: 30).id }
let!(:third_id) { source_model.create!(name: 'Sam', age: 40).id }
before do
stub_const("#{described_class.name}::BATCH_SIZE", 2)
-
- expect(migration).to receive(:queue_background_migration_jobs_by_range_at_intervals).and_call_original
+ stub_const("#{described_class.name}::SUB_BATCH_SIZE", 1)
end
it 'enqueues jobs to copy each batch of data' do
@@ -504,13 +503,13 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
Sidekiq::Testing.fake! do
migration.enqueue_partitioning_data_migration source_table
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
-
- first_job_arguments = [first_id, second_id, source_table.to_s, partitioned_table, 'id']
- expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([migration_class, first_job_arguments])
-
- second_job_arguments = [third_id, third_id, source_table.to_s, partitioned_table, 'id']
- expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([migration_class, second_job_arguments])
+ expect(migration_class).to have_scheduled_batched_migration(
+ table_name: source_table,
+ column_name: :id,
+ job_arguments: [partitioned_table],
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
end
end
end
diff --git a/spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb
index bc127f74e84..0844ab7eccc 100644
--- a/spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb
@@ -5,14 +5,15 @@ require 'spec_helper'
RSpec.describe Gitlab::LegacyGithubImport::UserFormatter do
let(:client) { double }
let(:octocat) { { id: 123456, login: 'octocat', email: 'octocat@example.com' } }
+ let(:gitea_ghost) { { id: -1, login: 'Ghost', email: '' } }
- subject(:user) { described_class.new(client, octocat) }
+ describe '#gitlab_id' do
+ subject(:user) { described_class.new(client, octocat) }
- before do
- allow(client).to receive(:user).and_return(octocat)
- end
+ before do
+ allow(client).to receive(:user).and_return(octocat)
+ end
- describe '#gitlab_id' do
context 'when GitHub user is a GitLab user' do
it 'return GitLab user id when user associated their account with GitHub' do
gl_user = create(:omniauth_user, extern_uid: octocat[:id], provider: 'github')
@@ -51,4 +52,16 @@ RSpec.describe Gitlab::LegacyGithubImport::UserFormatter do
expect(user.gitlab_id).to be_nil
end
end
+
+ describe '.email' do
+ subject(:user) { described_class.new(client, gitea_ghost) }
+
+ before do
+ allow(client).to receive(:user).and_return(gitea_ghost)
+ end
+
+ it 'assigns a dummy email address when user is a Ghost gitea user' do
+ expect(subject.send(:email)).to eq described_class::GITEA_GHOST_EMAIL
+ end
+ end
end