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>2021-03-09 15:08:52 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-03-09 15:08:52 +0300
commitb90d8b54a4d623e52cf1d4318023e3b18d13dd5b (patch)
tree19012dcd9dff32ef1c47182229bdabb6b078a7fc
parenta989894b49e6c72648485a82c570647c17a3763f (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/issue_templates/Experiment Successful Cleanup.md7
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/api.js15
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue2
-rw-r--r--app/assets/javascripts/badges/components/badge.vue2
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue4
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue2
-rw-r--r--app/assets/javascripts/projects/details/upload_button.vue49
-rw-r--r--app/controllers/projects/issues_controller.rb1
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/graphql/mutations/branches/create.rb2
-rw-r--r--app/graphql/resolvers/branch_commit_resolver.rb3
-rw-r--r--app/helpers/invite_members_helper.rb2
-rw-r--r--app/helpers/workhorse_helper.rb10
-rw-r--r--app/models/ci/job_artifact.rb6
-rw-r--r--app/models/commit_status.rb10
-rw-r--r--app/services/ci/process_pipeline_service.rb2
-rw-r--r--app/services/projects/destroy_service.rb7
-rw-r--r--app/services/projects/update_pages_service.rb1
-rw-r--r--app/views/shared/issuable/_sidebar_reviewers.html.haml15
-rw-r--r--changelogs/unreleased/321185-experiment-cleanup-invite_members_version_a-add-invite-members-to-.yml5
-rw-r--r--changelogs/unreleased/323848-fix-diff-comment-hidden-menu.yml5
-rw-r--r--changelogs/unreleased/btn-confirm-js-2fa.yml5
-rw-r--r--changelogs/unreleased/btn-confirm-js-badges.yml5
-rw-r--r--changelogs/unreleased/mc-bug-reduce-ci-minute-namespace-notification-db-load.yml5
-rw-r--r--changelogs/unreleased/mc-bug-remove-reliance-on-branch-context.yml5
-rw-r--r--changelogs/unreleased/pb-track-background-job-executions.yml5
-rw-r--r--changelogs/unreleased/remove-transactionless-destroy-feature-flag.yml5
-rw-r--r--config/feature_flags/development/attachment_with_filename.yml (renamed from config/feature_flags/development/project_transactionless_destroy.yml)10
-rw-r--r--config/feature_flags/development/ci_fix_commit_status_retried.yml8
-rw-r--r--config/feature_flags/development/ci_remove_update_retried_from_process_pipeline.yml8
-rw-r--r--config/feature_flags/experiment/invite_members_version_a_experiment_percentage.yml8
-rw-r--r--danger/documentation/Dangerfile3
-rw-r--r--db/migrate/20210128172149_create_background_migration_tracking_tables.rb59
-rw-r--r--db/schema_migrations/202101281721491
-rw-r--r--db/structure.sql75
-rw-r--r--doc/administration/geo/setup/database.md2
-rw-r--r--doc/development/testing_guide/frontend_testing.md51
-rw-r--r--lib/api/commit_statuses.rb7
-rw-r--r--lib/gitlab/background_migration/copy_column_using_background_migration_job.rb28
-rw-r--r--lib/gitlab/ci/features.rb4
-rw-r--r--lib/gitlab/database/background_migration/batched_job.rb23
-rw-r--r--lib/gitlab/database/background_migration/batched_migration.rb57
-rw-r--r--lib/gitlab/database/background_migration/batched_migration_wrapper.rb46
-rw-r--r--lib/gitlab/database/background_migration/primary_key_batching_strategy.rb27
-rw-r--r--lib/gitlab/database/background_migration/scheduler.rb60
-rw-r--r--lib/gitlab/database/migration_helpers.rb2
-rw-r--r--lib/gitlab/experimentation.rb4
-rw-r--r--lib/gitlab/git/commit.rb1
-rw-r--r--locale/gitlab.pot6
-rw-r--r--qa/qa/page/main/login.rb2
-rw-r--r--qa/qa/resource/personal_access_token.rb50
-rw-r--r--qa/qa/resource/user.rb15
-rw-r--r--qa/qa/runtime/api/client.rb38
-rw-r--r--qa/qa/runtime/env.rb26
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/jenkins/jenkins_build_status_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/maven_gradle_repository_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/maven_repository_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/npm_registry_spec.rb2
-rw-r--r--qa/qa/tools/initialize_gitlab_auth.rb2
-rw-r--r--qa/spec/runtime/api/client_spec.rb2
-rw-r--r--qa/spec/runtime/env_spec.rb1
-rw-r--r--spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb15
-rw-r--r--spec/factories/gitlab/database/background_migration/batched_jobs.rb12
-rw-r--r--spec/factories/gitlab/database/background_migration/batched_migrations.rb13
-rw-r--r--spec/frontend/api_spec.js14
-rw-r--r--spec/frontend/projects/details/upload_button_spec.js54
-rw-r--r--spec/graphql/resolvers/branch_commit_resolver_spec.rb4
-rw-r--r--spec/helpers/invite_members_helper_spec.rb10
-rw-r--r--spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb29
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_job_spec.rb50
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_spec.rb160
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb70
-rw-r--r--spec/lib/gitlab/database/background_migration/primary_key_batching_strategy_spec.rb44
-rw-r--r--spec/lib/gitlab/database/background_migration/scheduler_spec.rb182
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb2
-rw-r--r--spec/lib/gitlab/experimentation_spec.rb1
-rw-r--r--spec/models/ci/pipeline_spec.rb32
-rw-r--r--spec/requests/api/commit_statuses_spec.rb40
-rw-r--r--spec/requests/projects/merge_requests/content_spec.rb41
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb53
-rw-r--r--spec/services/projects/destroy_service_spec.rb474
-rw-r--r--spec/services/projects/update_pages_service_spec.rb35
-rw-r--r--spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb10
91 files changed, 1729 insertions, 478 deletions
diff --git a/.gitlab/issue_templates/Experiment Successful Cleanup.md b/.gitlab/issue_templates/Experiment Successful Cleanup.md
index 3f148ec00b1..b84bf1066f6 100644
--- a/.gitlab/issue_templates/Experiment Successful Cleanup.md
+++ b/.gitlab/issue_templates/Experiment Successful Cleanup.md
@@ -9,10 +9,11 @@ The changes need to become an official part of the product.
- [ ] Determine whether the feature should apply to SaaS and/or self-managed
- [ ] Determine whether the feature should apply to EE - and which tiers - and/or Core
-- [ ] Determine if tracking should be kept as is, removed, or modified.
-- [ ] Migrate experiment to a default enabled [feature flag](https://docs.gitlab.com/ee/development/feature_flags/development.html) for one milestone and add a changelog. Converting to a feature flag can be skipped at the ICs discretion if risk is deemed low with consideration to both SaaS and (if applicable) self managed.
+- [ ] Determine if tracking should be kept as is, removed, or modified
- [ ] Ensure any relevant documentation has been updated.
-- [ ] In the next milestone, [remove the feature flag](https://docs.gitlab.com/ee/development/feature_flags/controls.html#cleaning-up).
+- [ ] Consider changes to any `feature_category:` introduced by the experiment if ownership is changing (PM for Growth and PM for the new category as DRIs)
+- [ ] Optional: Migrate experiment to a default enabled [feature flag](https://docs.gitlab.com/ee/development/feature_flags/development.html) for one milestone and add a changelog. Converting to a feature flag can be skipped at the ICs discretion if risk is deemed low with consideration to both SaaS and (if applicable) self managed
+- [ ] In the next milestone, [remove the feature flag](https://docs.gitlab.com/ee/development/feature_flags/controls.html#cleaning-up) if applicable
- [ ] After the flag removal is deployed, [clean up the feature/experiment feature flags](https://docs.gitlab.com/ee/development/feature_flags/controls.html#cleaning-up) by running chatops command in `#production` channel
/label ~"feature" ~"feature::maintenance" ~"workflow::scheduling" ~"growth experiment" ~"feature flag"
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 3d008ee2c83..e823b7e3501 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-687bc5d8f36102e2c8033cce76094d5d318cd961
+059a82773ec2b5afc115442270d663cccc68451c
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 48005787d81..2b589b71163 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -31,6 +31,7 @@ const Api = {
projectLabelsPath: '/:namespace_path/:project_path/-/labels',
projectFileSchemaPath: '/:namespace_path/:project_path/-/schema/:ref/:filename',
projectUsersPath: '/api/:version/projects/:id/users',
+ projectGroupsPath: '/api/:version/projects/:id/groups.json',
projectInvitationsPath: '/api/:version/projects/:id/invitations',
projectMembersPath: '/api/:version/projects/:id/members',
projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests',
@@ -241,6 +242,20 @@ const Api = {
.then(({ data }) => data);
},
+ projectGroups(id, options) {
+ const url = Api.buildUrl(this.projectGroupsPath).replace(':id', encodeURIComponent(id));
+
+ return axios
+ .get(url, {
+ params: {
+ ...options,
+ },
+ })
+ .then(({ data }) => {
+ return data;
+ });
+ },
+
addProjectMembersByUserId(id, data) {
const url = Api.buildUrl(this.projectMembersPath).replace(':id', encodeURIComponent(id));
diff --git a/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue b/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
index 0e589d98668..55642aa64db 100644
--- a/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
+++ b/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
@@ -162,7 +162,7 @@ export default {
:href="profileAccountPath"
:disabled="proceedButtonDisabled"
:title="$options.i18n.proceedButton"
- variant="success"
+ variant="confirm"
data-qa-selector="proceed_button"
data-track-event="click_button"
:data-track-label="`${$options.trackingLabelPrefix}proceed_button`"
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
index c3512773457..9e5d70075f3 100644
--- a/app/assets/javascripts/badges/components/badge.vue
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -96,7 +96,7 @@ export default {
v-gl-tooltip.hover
:title="s__('Badges|Reload badge image')"
category="tertiary"
- variant="success"
+ variant="confirm"
type="button"
icon="retry"
size="small"
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index 20541ad8ccc..b65a8b4fa9c 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -225,7 +225,7 @@ export default {
<gl-button
:loading="isSaving"
type="submit"
- variant="success"
+ variant="confirm"
category="primary"
data-testid="saveEditing"
>
@@ -233,7 +233,7 @@ export default {
</gl-button>
</div>
<div v-else class="form-group">
- <gl-button :loading="isSaving" type="submit" variant="success" category="primary">
+ <gl-button :loading="isSaving" type="submit" variant="confirm" category="primary">
{{ s__('Badges|Add badge') }}
</gl-button>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue b/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue
index dfeda4aae7c..663163a7552 100644
--- a/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue
@@ -20,7 +20,7 @@ export default {
'li',
{
class:
- 'discussion-collapsible gl-border-solid gl-border-gray-100 gl-border-1 gl-rounded-base gl-overflow-hidden clearfix',
+ 'discussion-collapsible gl-border-solid gl-border-gray-100 gl-border-1 gl-rounded-base clearfix',
},
[h('ul', { class: 'notes' }, children)],
);
diff --git a/app/assets/javascripts/projects/details/upload_button.vue b/app/assets/javascripts/projects/details/upload_button.vue
new file mode 100644
index 00000000000..a89ea34c438
--- /dev/null
+++ b/app/assets/javascripts/projects/details/upload_button.vue
@@ -0,0 +1,49 @@
+<script>
+import { GlButton, GlModalDirective } from '@gitlab/ui';
+import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
+
+const UPLOAD_BLOB_MODAL_ID = 'details-modal-upload-blob';
+
+export default {
+ components: {
+ GlButton,
+ UploadBlobModal,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ inject: {
+ targetBranch: {
+ default: '',
+ },
+ origionalBranch: {
+ default: '',
+ },
+ canPushCode: {
+ default: false,
+ },
+ path: {
+ default: '',
+ },
+ projectPath: {
+ default: '',
+ },
+ },
+ uploadBlobModalId: UPLOAD_BLOB_MODAL_ID,
+};
+</script>
+<template>
+ <span>
+ <gl-button v-gl-modal="$options.uploadBlobModalId" icon="upload">{{
+ __('Upload File')
+ }}</gl-button>
+ <upload-blob-modal
+ :modal-id="$options.uploadBlobModalId"
+ :commit-message="__('Upload New File')"
+ :target-branch="targetBranch"
+ :origional-branch="origionalBranch"
+ :can-push-code="canPushCode"
+ :path="path"
+ />
+ </span>
+</template>
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index a02a1aaaf8b..17138da6e2b 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -53,7 +53,6 @@ class Projects::IssuesController < Projects::ApplicationController
push_to_gon_attributes(:features, real_time_feature_flag, real_time_enabled)
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
- record_experiment_user(:invite_members_version_a)
record_experiment_user(:invite_members_version_b)
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 40312f38e08..2c6d5f62b4e 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -44,7 +44,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:paginated_notes, @project, default_enabled: :yaml)
push_frontend_feature_flag(:new_pipelines_table, @project, default_enabled: :yaml)
- record_experiment_user(:invite_members_version_a)
record_experiment_user(:invite_members_version_b)
end
diff --git a/app/graphql/mutations/branches/create.rb b/app/graphql/mutations/branches/create.rb
index 6354976f1ea..a94d3966258 100644
--- a/app/graphql/mutations/branches/create.rb
+++ b/app/graphql/mutations/branches/create.rb
@@ -30,8 +30,6 @@ module Mutations
def resolve(project_path:, name:, ref:)
project = authorized_find!(project_path)
- context.scoped_set!(:branch_project, project)
-
result = ::Branches::CreateService.new(project, current_user)
.execute(name, ref)
diff --git a/app/graphql/resolvers/branch_commit_resolver.rb b/app/graphql/resolvers/branch_commit_resolver.rb
index 11c49e17bc5..4f6062a4781 100644
--- a/app/graphql/resolvers/branch_commit_resolver.rb
+++ b/app/graphql/resolvers/branch_commit_resolver.rb
@@ -10,8 +10,9 @@ module Resolvers
return unless branch
commit = branch.dereferenced_target
+ project = Project.find_by_full_path(commit.repository.gl_project_path)
- ::Commit.new(commit, context[:branch_project]) if commit
+ ::Commit.new(commit, project) if commit
end
end
end
diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb
index 961ef613a06..62d83ebe79e 100644
--- a/app/helpers/invite_members_helper.rb
+++ b/app/helpers/invite_members_helper.rb
@@ -13,7 +13,7 @@ module InviteMembersHelper
def directly_invite_members?
strong_memoize(:directly_invite_members) do
- experiment_enabled?(:invite_members_version_a) && can_import_members?
+ can_import_members?
end
end
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index f74b53d68a1..28dd1b00292 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -7,7 +7,7 @@ module WorkhorseHelper
def send_git_blob(repository, blob, inline: true)
headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob))
- headers['Content-Disposition'] = inline ? 'inline' : 'attachment'
+ headers['Content-Disposition'] = inline ? 'inline' : content_disposition_attachment(repository.project, blob.name)
# If enabled, this will override the values set above
workhorse_set_content_type!
@@ -48,4 +48,12 @@ module WorkhorseHelper
def workhorse_set_content_type!
headers[Gitlab::Workhorse::DETECT_HEADER] = "true"
end
+
+ def content_disposition_attachment(project, filename)
+ if Feature.enabled?(:attachment_with_filename, project, default_enabled: :yaml)
+ ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: filename)
+ else
+ 'attachment'
+ end
+ end
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index f927111758a..06ea2a7f951 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -136,11 +136,7 @@ module Ci
scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) }
scope :for_job_name, ->(name) { joins(:job).where(ci_builds: { name: name }) }
- scope :with_job, -> do
- if Feature.enabled?(:non_public_artifacts, type: :development)
- joins(:job).includes(:job)
- end
- end
+ scope :with_job, -> { joins(:job).includes(:job) }
scope :with_file_types, -> (file_types) do
types = self.file_types.select { |file_type| file_types.include?(file_type) }.values
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 781b3456c87..524429bf12a 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -84,6 +84,8 @@ class CommitStatus < ApplicationRecord
# extend this `Hash` with new values.
enum_with_nil failure_reason: Enums::Ci::CommitStatus.failure_reasons
+ default_value_for :retried, false
+
##
# We still create some CommitStatuses outside of CreatePipelineService.
#
@@ -290,6 +292,14 @@ class CommitStatus < ApplicationRecord
failed? && !unrecoverable_failure?
end
+ def update_older_statuses_retried!
+ self.class
+ .latest
+ .where(name: name)
+ .where.not(id: id)
+ .update_all(retried: true, processed: true)
+ end
+
private
def unrecoverable_failure?
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 678b386fbbf..970652b4da3 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -30,6 +30,8 @@ module Ci
# this updates only when there are data that needs to be updated, there are two groups with no retried flag
# rubocop: disable CodeReuse/ActiveRecord
def update_retried
+ return if Feature.enabled?(:ci_remove_update_retried_from_process_pipeline, pipeline.project, default_enabled: :yaml)
+
# find the latest builds for each name
latest_statuses = pipeline.latest_statuses
.group(:name)
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index c1501625300..6840c395a76 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -107,12 +107,7 @@ module Projects
end
project.leave_pool_repository
-
- if Gitlab::Ci::Features.project_transactionless_destroy?(project)
- destroy_project_related_records(project)
- else
- Project.transaction { destroy_project_related_records(project) }
- end
+ destroy_project_related_records(project)
end
def destroy_project_related_records(project)
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 29e92d725e2..2b59fdd539d 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -33,6 +33,7 @@ module Projects
@status = create_status
@status.enqueue!
@status.run!
+ @status.update_older_statuses_retried! if Feature.enabled?(:ci_fix_commit_status_retried, project, default_enabled: :yaml)
raise InvalidStateError, 'missing pages artifacts' unless build.artifacts?
raise InvalidStateError, 'build SHA is outdated for this ref' unless latest?
diff --git a/app/views/shared/issuable/_sidebar_reviewers.html.haml b/app/views/shared/issuable/_sidebar_reviewers.html.haml
index 0142c87aeb0..1a8f1a2639f 100644
--- a/app/views/shared/issuable/_sidebar_reviewers.html.haml
+++ b/app/views/shared/issuable/_sidebar_reviewers.html.haml
@@ -39,17 +39,4 @@
- data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
- options[:data].merge!(data)
- - if experiment_enabled?(:invite_members_version_a) && can_import_members?
- - options[:dropdown_class] += ' dropdown-extended-height'
- - options[:footer_content] = true
- - options[:wrapper_class] = 'js-sidebar-reviewer-dropdown'
-
- = dropdown_tag(title, options: options) do
- %ul.dropdown-footer-list
- %li
- = link_to _('Invite Members'),
- project_project_members_path(@project),
- title: _('Invite Members'),
- data: { 'is-link': true, 'track-event': 'click_invite_members', 'track-label': 'edit_reviewer' }
- - else
- = dropdown_tag(title, options: options)
+ = dropdown_tag(title, options: options)
diff --git a/changelogs/unreleased/321185-experiment-cleanup-invite_members_version_a-add-invite-members-to-.yml b/changelogs/unreleased/321185-experiment-cleanup-invite_members_version_a-add-invite-members-to-.yml
new file mode 100644
index 00000000000..6dc3c991128
--- /dev/null
+++ b/changelogs/unreleased/321185-experiment-cleanup-invite_members_version_a-add-invite-members-to-.yml
@@ -0,0 +1,5 @@
+---
+title: Cleanup invite_members_version_a experiment
+merge_request: 55178
+author:
+type: added
diff --git a/changelogs/unreleased/323848-fix-diff-comment-hidden-menu.yml b/changelogs/unreleased/323848-fix-diff-comment-hidden-menu.yml
new file mode 100644
index 00000000000..7f32410cf75
--- /dev/null
+++ b/changelogs/unreleased/323848-fix-diff-comment-hidden-menu.yml
@@ -0,0 +1,5 @@
+---
+title: Fix diff comment hidden dropdown
+merge_request: 56072
+author:
+type: fixed
diff --git a/changelogs/unreleased/btn-confirm-js-2fa.yml b/changelogs/unreleased/btn-confirm-js-2fa.yml
new file mode 100644
index 00000000000..ea56cd1df0f
--- /dev/null
+++ b/changelogs/unreleased/btn-confirm-js-2fa.yml
@@ -0,0 +1,5 @@
+---
+title: Move to confirm variant from success in 2fa codes component
+merge_request: 55729
+author: Yogi (@yo)
+type: changed
diff --git a/changelogs/unreleased/btn-confirm-js-badges.yml b/changelogs/unreleased/btn-confirm-js-badges.yml
new file mode 100644
index 00000000000..a20bb68fa43
--- /dev/null
+++ b/changelogs/unreleased/btn-confirm-js-badges.yml
@@ -0,0 +1,5 @@
+---
+title: Move to confirm variant from success in badges component
+merge_request: 55730
+author: Yogi (@yo)
+type: changed
diff --git a/changelogs/unreleased/mc-bug-reduce-ci-minute-namespace-notification-db-load.yml b/changelogs/unreleased/mc-bug-reduce-ci-minute-namespace-notification-db-load.yml
new file mode 100644
index 00000000000..9707dc4623e
--- /dev/null
+++ b/changelogs/unreleased/mc-bug-reduce-ci-minute-namespace-notification-db-load.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce DB load when resetting CI minute notifications.
+merge_request: 55765
+author:
+type: performance
diff --git a/changelogs/unreleased/mc-bug-remove-reliance-on-branch-context.yml b/changelogs/unreleased/mc-bug-remove-reliance-on-branch-context.yml
new file mode 100644
index 00000000000..a4631a8394f
--- /dev/null
+++ b/changelogs/unreleased/mc-bug-remove-reliance-on-branch-context.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve project from branch in commit resolver.
+merge_request: 55694
+author:
+type: fixed
diff --git a/changelogs/unreleased/pb-track-background-job-executions.yml b/changelogs/unreleased/pb-track-background-job-executions.yml
new file mode 100644
index 00000000000..bedf6317e8d
--- /dev/null
+++ b/changelogs/unreleased/pb-track-background-job-executions.yml
@@ -0,0 +1,5 @@
+---
+title: Create tables to track auto-batched background migrations
+merge_request: 54628
+author:
+type: added
diff --git a/changelogs/unreleased/remove-transactionless-destroy-feature-flag.yml b/changelogs/unreleased/remove-transactionless-destroy-feature-flag.yml
new file mode 100644
index 00000000000..5ae09c3e2f1
--- /dev/null
+++ b/changelogs/unreleased/remove-transactionless-destroy-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Remove project_transactionless_destroy feature flag
+merge_request: 55795
+author:
+type: changed
diff --git a/config/feature_flags/development/project_transactionless_destroy.yml b/config/feature_flags/development/attachment_with_filename.yml
index d51db437356..8d3a96404ef 100644
--- a/config/feature_flags/development/project_transactionless_destroy.yml
+++ b/config/feature_flags/development/attachment_with_filename.yml
@@ -1,8 +1,8 @@
---
-name: project_transactionless_destroy
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39367
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/255972
-milestone: '13.4'
+name: attachment_with_filename
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55066
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323714
+milestone: '13.10'
type: development
-group: group::continuous integration
+group: group::editor
default_enabled: false
diff --git a/config/feature_flags/development/ci_fix_commit_status_retried.yml b/config/feature_flags/development/ci_fix_commit_status_retried.yml
new file mode 100644
index 00000000000..85b1836b065
--- /dev/null
+++ b/config/feature_flags/development/ci_fix_commit_status_retried.yml
@@ -0,0 +1,8 @@
+---
+name: ci_fix_commit_status_retried
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54300
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321631
+milestone: '13.9'
+type: development
+group: group::pipeline authoring
+default_enabled: false
diff --git a/config/feature_flags/development/ci_remove_update_retried_from_process_pipeline.yml b/config/feature_flags/development/ci_remove_update_retried_from_process_pipeline.yml
new file mode 100644
index 00000000000..82470baf6b4
--- /dev/null
+++ b/config/feature_flags/development/ci_remove_update_retried_from_process_pipeline.yml
@@ -0,0 +1,8 @@
+---
+name: ci_remove_update_retried_from_process_pipeline
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54300
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321630
+milestone: '13.9'
+type: development
+group: group::pipeline authoring
+default_enabled: false
diff --git a/config/feature_flags/experiment/invite_members_version_a_experiment_percentage.yml b/config/feature_flags/experiment/invite_members_version_a_experiment_percentage.yml
deleted file mode 100644
index d20cb10fcda..00000000000
--- a/config/feature_flags/experiment/invite_members_version_a_experiment_percentage.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: invite_members_version_a_experiment_percentage
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33210
-rollout_issue_url: https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/299
-milestone: '13.1'
-type: experiment
-group: group::expansion
-default_enabled: false
diff --git a/danger/documentation/Dangerfile b/danger/documentation/Dangerfile
index 240c374435c..df7d0337e94 100644
--- a/danger/documentation/Dangerfile
+++ b/danger/documentation/Dangerfile
@@ -41,6 +41,7 @@ markdown(<<~MARKDOWN)
The review does not need to block merging this merge request. See the:
- - [Technical Writers assignments](https://about.gitlab.com/handbook/engineering/technical-writing/#designated-technical-writers) for the appropriate technical writer for this review.
+ - [Metadata for the `*.md` files](https://docs.gitlab.com/ee/development/documentation/#metadata) that you've changed. The first few lines of each `*.md` file identify the stage and group most closely associated with your docs change.
+ - The [Technical Writer assigned](https://about.gitlab.com/handbook/engineering/technical-writing/#designated-technical-writers) for that stage and group.
- [Documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html) for information on when to assign a merge request for review.
MARKDOWN
diff --git a/db/migrate/20210128172149_create_background_migration_tracking_tables.rb b/db/migrate/20210128172149_create_background_migration_tracking_tables.rb
new file mode 100644
index 00000000000..767bd8737a3
--- /dev/null
+++ b/db/migrate/20210128172149_create_background_migration_tracking_tables.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+class CreateBackgroundMigrationTrackingTables < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table_with_constraints :batched_background_migrations do |t|
+ t.timestamps_with_timezone
+ t.bigint :min_value, null: false, default: 1
+ t.bigint :max_value, null: false
+ t.integer :batch_size, null: false
+ t.integer :sub_batch_size, null: false
+ t.integer :interval, limit: 2, null: false
+ t.integer :status, limit: 2, null: false, default: 0
+ t.text :job_class_name, null: false
+ t.text :batch_class_name, null: false,
+ default: 'Gitlab::Database::BackgroundMigration::PrimaryKeyBatchingStrategy'
+ t.text :table_name, null: false
+ t.text :column_name, null: false
+ t.jsonb :job_arguments, null: false, default: '[]'
+
+ t.text_limit :job_class_name, 100
+ t.text_limit :batch_class_name, 100
+ t.text_limit :table_name, 63
+ t.text_limit :column_name, 63
+
+ t.check_constraint :check_positive_min_value, 'min_value > 0'
+ t.check_constraint :check_max_value_in_range, 'max_value >= min_value'
+
+ t.check_constraint :check_positive_sub_batch_size, 'sub_batch_size > 0'
+ t.check_constraint :check_batch_size_in_range, 'batch_size >= sub_batch_size'
+
+ t.index %i[job_class_name table_name column_name], name: :index_batched_migrations_on_job_table_and_column_name
+ end
+
+ create_table :batched_background_migration_jobs do |t|
+ t.timestamps_with_timezone
+ t.datetime_with_timezone :started_at
+ t.datetime_with_timezone :finished_at
+ t.references :batched_background_migration, null: false, index: false, foreign_key: { on_delete: :cascade }
+ t.bigint :min_value, null: false
+ t.bigint :max_value, null: false
+ t.integer :batch_size, null: false
+ t.integer :sub_batch_size, null: false
+ t.integer :status, limit: 2, null: false, default: 0
+ t.integer :attempts, limit: 2, null: false, default: 0
+
+ t.index [:batched_background_migration_id, :id], name: :index_batched_jobs_by_batched_migration_id_and_id
+ end
+ end
+
+ def down
+ drop_table :batched_background_migration_jobs
+
+ drop_table :batched_background_migrations
+ end
+end
diff --git a/db/schema_migrations/20210128172149 b/db/schema_migrations/20210128172149
new file mode 100644
index 00000000000..cc0e050be3e
--- /dev/null
+++ b/db/schema_migrations/20210128172149
@@ -0,0 +1 @@
+1cf1305ad5eaaef51f99f057b8a2e81731d69a6d02629c0c9a7d94dfdecbea47 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 3d0e8b903cb..8a7ddfdfa02 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9768,6 +9768,64 @@ CREATE SEQUENCE badges_id_seq
ALTER SEQUENCE badges_id_seq OWNED BY badges.id;
+CREATE TABLE batched_background_migration_jobs (
+ id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ started_at timestamp with time zone,
+ finished_at timestamp with time zone,
+ batched_background_migration_id bigint NOT NULL,
+ min_value bigint NOT NULL,
+ max_value bigint NOT NULL,
+ batch_size integer NOT NULL,
+ sub_batch_size integer NOT NULL,
+ status smallint DEFAULT 0 NOT NULL,
+ attempts smallint DEFAULT 0 NOT NULL
+);
+
+CREATE SEQUENCE batched_background_migration_jobs_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE batched_background_migration_jobs_id_seq OWNED BY batched_background_migration_jobs.id;
+
+CREATE TABLE batched_background_migrations (
+ id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ min_value bigint DEFAULT 1 NOT NULL,
+ max_value bigint NOT NULL,
+ batch_size integer NOT NULL,
+ sub_batch_size integer NOT NULL,
+ "interval" smallint NOT NULL,
+ status smallint DEFAULT 0 NOT NULL,
+ job_class_name text NOT NULL,
+ batch_class_name text DEFAULT 'Gitlab::Database::BackgroundMigration::PrimaryKeyBatchingStrategy'::text NOT NULL,
+ table_name text NOT NULL,
+ column_name text NOT NULL,
+ job_arguments jsonb DEFAULT '"[]"'::jsonb NOT NULL,
+ CONSTRAINT check_5bb0382d6f CHECK ((char_length(column_name) <= 63)),
+ CONSTRAINT check_6b6a06254a CHECK ((char_length(table_name) <= 63)),
+ CONSTRAINT check_batch_size_in_range CHECK ((batch_size >= sub_batch_size)),
+ CONSTRAINT check_e6c75b1e29 CHECK ((char_length(job_class_name) <= 100)),
+ CONSTRAINT check_fe10674721 CHECK ((char_length(batch_class_name) <= 100)),
+ CONSTRAINT check_max_value_in_range CHECK ((max_value >= min_value)),
+ CONSTRAINT check_positive_min_value CHECK ((min_value > 0)),
+ CONSTRAINT check_positive_sub_batch_size CHECK ((sub_batch_size > 0))
+);
+
+CREATE SEQUENCE batched_background_migrations_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE batched_background_migrations_id_seq OWNED BY batched_background_migrations.id;
+
CREATE TABLE board_assignees (
id integer NOT NULL,
board_id integer NOT NULL,
@@ -18812,6 +18870,10 @@ ALTER TABLE ONLY background_migration_jobs ALTER COLUMN id SET DEFAULT nextval('
ALTER TABLE ONLY badges ALTER COLUMN id SET DEFAULT nextval('badges_id_seq'::regclass);
+ALTER TABLE ONLY batched_background_migration_jobs ALTER COLUMN id SET DEFAULT nextval('batched_background_migration_jobs_id_seq'::regclass);
+
+ALTER TABLE ONLY batched_background_migrations ALTER COLUMN id SET DEFAULT nextval('batched_background_migrations_id_seq'::regclass);
+
ALTER TABLE ONLY board_assignees ALTER COLUMN id SET DEFAULT nextval('board_assignees_id_seq'::regclass);
ALTER TABLE ONLY board_group_recent_visits ALTER COLUMN id SET DEFAULT nextval('board_group_recent_visits_id_seq'::regclass);
@@ -19893,6 +19955,12 @@ ALTER TABLE ONLY background_migration_jobs
ALTER TABLE ONLY badges
ADD CONSTRAINT badges_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY batched_background_migration_jobs
+ ADD CONSTRAINT batched_background_migration_jobs_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY batched_background_migrations
+ ADD CONSTRAINT batched_background_migrations_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY board_assignees
ADD CONSTRAINT board_assignees_pkey PRIMARY KEY (id);
@@ -21641,6 +21709,10 @@ CREATE INDEX index_badges_on_group_id ON badges USING btree (group_id);
CREATE INDEX index_badges_on_project_id ON badges USING btree (project_id);
+CREATE INDEX index_batched_jobs_by_batched_migration_id_and_id ON batched_background_migration_jobs USING btree (batched_background_migration_id, id);
+
+CREATE INDEX index_batched_migrations_on_job_table_and_column_name ON batched_background_migrations USING btree (job_class_name, table_name, column_name);
+
CREATE INDEX index_board_assignees_on_assignee_id ON board_assignees USING btree (assignee_id);
CREATE UNIQUE INDEX index_board_assignees_on_board_id_and_assignee_id ON board_assignees USING btree (board_id, assignee_id);
@@ -25383,6 +25455,9 @@ ALTER TABLE ONLY ci_resources
ALTER TABLE ONLY clusters_applications_fluentd
ADD CONSTRAINT fk_rails_4319b1dcd2 FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE CASCADE;
+ALTER TABLE ONLY batched_background_migration_jobs
+ ADD CONSTRAINT fk_rails_432153b86d FOREIGN KEY (batched_background_migration_id) REFERENCES batched_background_migrations(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY operations_strategies_user_lists
ADD CONSTRAINT fk_rails_43241e8d29 FOREIGN KEY (strategy_id) REFERENCES operations_strategies(id) ON DELETE CASCADE;
diff --git a/doc/administration/geo/setup/database.md b/doc/administration/geo/setup/database.md
index 1272e7d1419..7128d4283d1 100644
--- a/doc/administration/geo/setup/database.md
+++ b/doc/administration/geo/setup/database.md
@@ -496,7 +496,7 @@ A production-ready and secure setup requires at least three Consul nodes, three
Patroni nodes, one internal load-balancing node on the primary site, and a similar
configuration for the secondary site. The internal load balancer provides a single
endpoint for connecting to the Patroni cluster's leader whenever a new leader is
-elected. Be sure to use [password credentials](../..//postgresql/replication_and_failover.md#database-authorization-for-patroni) and other database best practices.
+elected. Be sure to use [password credentials](../../postgresql/replication_and_failover.md#database-authorization-for-patroni) and other database best practices.
Similar to `repmgr`, using Patroni on a secondary node is optional.
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index c765e214804..059983f790e 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -126,7 +126,7 @@ It does not make sense to test our `getFahrenheit` function because underneath i
Let's take a short look into Vue land. Vue is a critical part of the GitLab JavaScript codebase. When writing specs for Vue components, a common gotcha is to actually end up testing Vue provided functionality, because it appears to be the easiest thing to test. Here's an example taken from our codebase.
```javascript
-// Component
+// Component script
{
computed: {
hasMetricTypes() {
@@ -135,27 +135,46 @@ Let's take a short look into Vue land. Vue is a critical part of the GitLab Java
}
```
-and here's the corresponding spec
+```html
+<!-- Component template -->
+<template>
+ <gl-dropdown v-if="hasMetricTypes">
+ <!-- Dropdown content -->
+ </gl-dropdown>
+</template>
+```
+
+Testing the `hasMetricTypes` computed prop would seem like a given here. But to test if the computed property is returning the length of `metricTypes`, is testing the Vue library itself. There is no value in this, besides it adding to the test suite. It's better to test a component in the way the user interacts with it: checking the rendered template.
```javascript
- describe('computed', () => {
- describe('hasMetricTypes', () => {
- it('returns true if metricTypes exist', () => {
- factory({ metricTypes });
- expect(wrapper.vm.hasMetricTypes).toBe(2);
- });
-
- it('returns true if no metricTypes exist', () => {
- factory();
- expect(wrapper.vm.hasMetricTypes).toBe(0);
- });
+// Bad
+describe('computed', () => {
+ describe('hasMetricTypes', () => {
+ it('returns true if metricTypes exist', () => {
+ factory({ metricTypes });
+ expect(wrapper.vm.hasMetricTypes).toBe(2);
});
+
+ it('returns true if no metricTypes exist', () => {
+ factory();
+ expect(wrapper.vm.hasMetricTypes).toBe(0);
+ });
+ });
+});
+
+// Good
+it('displays a dropdown if metricTypes exist', () => {
+ factory({ metricTypes });
+ expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
});
-```
-Testing the `hasMetricTypes` computed prop would seem like a given, but to test if the computed property is returning the length of `metricTypes`, is testing the Vue library itself. There is no value in this, besides it adding to the test suite. Better is to test it in the way the user interacts with it. Probably through the template.
+it('does not display a dropdown if no metricTypes exist', () => {
+ factory();
+ expect(wrapper.findComponent(GlDropdown).exists()).toBe(false);
+});
+```
-Keep an eye out for these kinds of tests, as they just make updating logic more fragile and tedious than it needs to be. This is also true for other libraries.
+Keep an eye out for these kinds of tests, as they just make updating logic more fragile and tedious than it needs to be. This is also true for other libraries. A rule of thumb here is: if you are checking a `wrapper.vm` property, you should probably stop and rethink the test to check the rendered template instead.
Some more examples can be found in the [Frontend unit tests section](testing_levels.md#frontend-unit-tests)
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 26af921432c..e199111c975 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -100,7 +100,12 @@ module API
attributes_for_keys(%w[target_url description coverage])
status.update(optional_attributes) if optional_attributes.any?
- render_validation_error!(status) if status.invalid?
+
+ if status.valid?
+ status.update_older_statuses_retried! if Feature.enabled?(:ci_fix_commit_status_retried, user_project, default_enabled: :yaml)
+ else
+ render_validation_error!(status)
+ end
begin
case params[:state]
diff --git a/lib/gitlab/background_migration/copy_column_using_background_migration_job.rb b/lib/gitlab/background_migration/copy_column_using_background_migration_job.rb
index 16c0de39a3b..60682bd2ec1 100644
--- a/lib/gitlab/background_migration/copy_column_using_background_migration_job.rb
+++ b/lib/gitlab/background_migration/copy_column_using_background_migration_job.rb
@@ -2,13 +2,11 @@
module Gitlab
module BackgroundMigration
- # Background migration that extends CopyColumn to update the value of a
+ # Background migration that updates the value of a
# column using the value of another column in the same table.
#
# - The {start_id, end_id} arguments are at the start so that it can be used
- # with `queue_background_migration_jobs_by_range_at_intervals`
- # - Provides support for background job tracking through the use of
- # Gitlab::Database::BackgroundMigrationJob
+ # with `queue_batched_background_migration`
# - Uses sub-batching so that we can keep each update's execution time at
# low 100s ms, while being able to update more records per 2 minutes
# that we allow background migration jobs to be scheduled one after the other
@@ -22,28 +20,24 @@ module Gitlab
# start_id - The start ID of the range of rows to update.
# end_id - The end ID of the range of rows to update.
- # table - The name of the table that contains the columns.
- # primary_key - The primary key column of the table.
- # copy_from - The column containing the data to copy.
- # copy_to - The column to copy the data to.
+ # batch_table - The name of the table that contains the columns.
+ # batch_column - The name of the column we use to batch over the table.
# sub_batch_size - We don't want updates to take more than ~100ms
# This allows us to run multiple smaller batches during
# the minimum 2.minute interval that we can schedule jobs
- def perform(start_id, end_id, table, primary_key, copy_from, copy_to, sub_batch_size)
+ # copy_from - The column containing the data to copy.
+ # copy_to - The column to copy the data to.
+ def perform(start_id, end_id, batch_table, batch_column, sub_batch_size, copy_from, copy_to)
quoted_copy_from = connection.quote_column_name(copy_from)
quoted_copy_to = connection.quote_column_name(copy_to)
- parent_batch_relation = relation_scoped_to_range(table, primary_key, start_id, end_id)
+ parent_batch_relation = relation_scoped_to_range(batch_table, batch_column, start_id, end_id)
- parent_batch_relation.each_batch(column: primary_key, of: sub_batch_size) do |sub_batch|
+ parent_batch_relation.each_batch(column: batch_column, of: sub_batch_size) do |sub_batch|
sub_batch.update_all("#{quoted_copy_to}=#{quoted_copy_from}")
sleep(PAUSE_SECONDS)
end
-
- # We have to add all arguments when marking a job as succeeded as they
- # are all used to track the job by `queue_background_migration_jobs_by_range_at_intervals`
- mark_job_as_succeeded(start_id, end_id, table, primary_key, copy_from, copy_to, sub_batch_size)
end
private
@@ -52,10 +46,6 @@ module Gitlab
ActiveRecord::Base.connection
end
- def mark_job_as_succeeded(*arguments)
- Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(self.class.name, arguments)
- end
-
def relation_scoped_to_range(source_table, source_key_column, start_id, stop_id)
define_batchable_model(source_table).where(source_key_column => start_id..stop_id)
end
diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb
index 021ec02fb84..27ea9c2558a 100644
--- a/lib/gitlab/ci/features.rb
+++ b/lib/gitlab/ci/features.rb
@@ -38,10 +38,6 @@ module Gitlab
::Feature.enabled?(:ci_disallow_to_create_merge_request_pipelines_in_target_project, target_project)
end
- def self.project_transactionless_destroy?(project)
- Feature.enabled?(:project_transactionless_destroy, project, default_enabled: false)
- end
-
def self.trace_overwrite?
::Feature.enabled?(:ci_trace_overwrite, type: :ops, default_enabled: false)
end
diff --git a/lib/gitlab/database/background_migration/batched_job.rb b/lib/gitlab/database/background_migration/batched_job.rb
new file mode 100644
index 00000000000..3b624df2bfd
--- /dev/null
+++ b/lib/gitlab/database/background_migration/batched_job.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module BackgroundMigration
+ class BatchedJob < ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord
+ self.table_name = :batched_background_migration_jobs
+
+ belongs_to :batched_migration, foreign_key: :batched_background_migration_id
+
+ enum status: {
+ pending: 0,
+ running: 1,
+ failed: 2,
+ succeeded: 3
+ }
+
+ delegate :aborted?, :job_class, :table_name, :column_name, :job_arguments,
+ to: :batched_migration, prefix: :migration
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/background_migration/batched_migration.rb b/lib/gitlab/database/background_migration/batched_migration.rb
new file mode 100644
index 00000000000..316a6dafeea
--- /dev/null
+++ b/lib/gitlab/database/background_migration/batched_migration.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module BackgroundMigration
+ class BatchedMigration < ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord
+ self.table_name = :batched_background_migrations
+
+ has_many :batched_jobs, foreign_key: :batched_background_migration_id
+ has_one :last_job, -> { order(id: :desc) },
+ class_name: 'Gitlab::Database::BackgroundMigration::BatchedJob',
+ foreign_key: :batched_background_migration_id
+
+ scope :queue_order, -> { order(id: :asc) }
+
+ enum status: {
+ paused: 0,
+ active: 1,
+ aborted: 2,
+ finished: 3
+ }
+
+ def self.remove_toplevel_prefix(name)
+ name&.sub(/\A::/, '')
+ end
+
+ def interval_elapsed?
+ last_job.nil? || last_job.created_at <= Time.current - interval
+ end
+
+ def create_batched_job!(min, max)
+ batched_jobs.create!(min_value: min, max_value: max, batch_size: batch_size, sub_batch_size: sub_batch_size)
+ end
+
+ def next_min_value
+ last_job&.max_value&.next || min_value
+ end
+
+ def job_class
+ job_class_name.constantize
+ end
+
+ def batch_class
+ batch_class_name.constantize
+ end
+
+ def job_class_name=(class_name)
+ write_attribute(:job_class_name, self.class.remove_toplevel_prefix(class_name))
+ end
+
+ def batch_class_name=(class_name)
+ write_attribute(:batch_class_name, self.class.remove_toplevel_prefix(class_name))
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/background_migration/batched_migration_wrapper.rb b/lib/gitlab/database/background_migration/batched_migration_wrapper.rb
new file mode 100644
index 00000000000..299bd992197
--- /dev/null
+++ b/lib/gitlab/database/background_migration/batched_migration_wrapper.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module BackgroundMigration
+ class BatchedMigrationWrapper
+ def perform(batch_tracking_record)
+ start_tracking_execution(batch_tracking_record)
+
+ execute_batch(batch_tracking_record)
+
+ batch_tracking_record.status = :succeeded
+ rescue => e
+ batch_tracking_record.status = :failed
+
+ raise e
+ ensure
+ finish_tracking_execution(batch_tracking_record)
+ end
+
+ private
+
+ def start_tracking_execution(tracking_record)
+ tracking_record.update!(attempts: tracking_record.attempts + 1, status: :running, started_at: Time.current)
+ end
+
+ def execute_batch(tracking_record)
+ job_instance = tracking_record.migration_job_class.new
+
+ job_instance.perform(
+ tracking_record.min_value,
+ tracking_record.max_value,
+ tracking_record.migration_table_name,
+ tracking_record.migration_column_name,
+ tracking_record.sub_batch_size,
+ *tracking_record.migration_job_arguments)
+ end
+
+ def finish_tracking_execution(tracking_record)
+ tracking_record.finished_at = Time.current
+ tracking_record.save!
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/background_migration/primary_key_batching_strategy.rb b/lib/gitlab/database/background_migration/primary_key_batching_strategy.rb
new file mode 100644
index 00000000000..46af1371dad
--- /dev/null
+++ b/lib/gitlab/database/background_migration/primary_key_batching_strategy.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module BackgroundMigration
+ class PrimaryKeyBatchingStrategy
+ include Gitlab::Database::DynamicModelHelpers
+
+ def next_batch(table_name, column_name, batch_min_value:, batch_size:)
+ model_class = define_batchable_model(table_name)
+
+ quoted_column_name = model_class.connection.quote_column_name(column_name)
+ relation = model_class.where("#{quoted_column_name} >= ?", batch_min_value)
+ next_batch_bounds = nil
+
+ relation.each_batch(of: batch_size, column: column_name) do |batch| # rubocop:disable Lint/UnreachableLoop
+ next_batch_bounds = batch.pluck(Arel.sql("MIN(#{quoted_column_name}), MAX(#{quoted_column_name})")).first
+
+ break
+ end
+
+ next_batch_bounds
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/background_migration/scheduler.rb b/lib/gitlab/database/background_migration/scheduler.rb
new file mode 100644
index 00000000000..5f8a5ec06a5
--- /dev/null
+++ b/lib/gitlab/database/background_migration/scheduler.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module BackgroundMigration
+ class Scheduler
+ def perform(migration_wrapper: BatchedMigrationWrapper.new)
+ active_migration = BatchedMigration.active.queue_order.first
+
+ return unless active_migration&.interval_elapsed?
+
+ if next_batched_job = create_next_batched_job!(active_migration)
+ migration_wrapper.perform(next_batched_job)
+ else
+ finish_active_migration(active_migration)
+ end
+ end
+
+ private
+
+ def create_next_batched_job!(active_migration)
+ next_batch_range = find_next_batch_range(active_migration)
+
+ return if next_batch_range.nil?
+
+ active_migration.create_batched_job!(next_batch_range.min, next_batch_range.max)
+ end
+
+ def find_next_batch_range(active_migration)
+ batching_strategy = active_migration.batch_class.new
+ batch_min_value = active_migration.next_min_value
+
+ next_batch_bounds = batching_strategy.next_batch(
+ active_migration.table_name,
+ active_migration.column_name,
+ batch_min_value: batch_min_value,
+ batch_size: active_migration.batch_size)
+
+ return if next_batch_bounds.nil?
+
+ clamped_batch_range(active_migration, next_batch_bounds)
+ end
+
+ def clamped_batch_range(active_migration, next_bounds)
+ min_value, max_value = next_bounds
+
+ return if min_value > active_migration.max_value
+
+ max_value = max_value.clamp(min_value, active_migration.max_value)
+
+ (min_value..max_value)
+ end
+
+ def finish_active_migration(active_migration)
+ active_migration.finished!
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 6b169a504f3..e8ed3bb1258 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -1015,7 +1015,7 @@ module Gitlab
'CopyColumnUsingBackgroundMigrationJob',
interval,
batch_size: batch_size,
- other_job_arguments: [table, primary_key, column, tmp_column, sub_batch_size],
+ other_job_arguments: [table, primary_key, sub_batch_size, column, tmp_column],
track_jobs: true,
primary_column_name: primary_key
)
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 387eaba9861..068d93a8c04 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -38,10 +38,6 @@ module Gitlab
tracking_category: 'Growth::Expansion::Experiment::UpgradeLinkInUserMenuA',
use_backwards_compatible_subject_index: true
},
- invite_members_version_a: {
- tracking_category: 'Growth::Expansion::Experiment::InviteMembersVersionA',
- use_backwards_compatible_subject_index: true
- },
invite_members_version_b: {
tracking_category: 'Growth::Expansion::Experiment::InviteMembersVersionB',
use_backwards_compatible_subject_index: true
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 35c3dc5b0b3..ff99803d8de 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -20,6 +20,7 @@ module Gitlab
].freeze
attr_accessor(*SERIALIZE_KEYS)
+ attr_reader :repository
def ==(other)
return false unless other.is_a?(Gitlab::Git::Commit)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index fa34a48690d..7f2e2d7da27 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -30946,9 +30946,6 @@ msgstr ""
msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
-msgstr ""
-
msgid "ThreatMonitoring|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
msgstr ""
@@ -32227,6 +32224,9 @@ msgstr ""
msgid "Upload CSV file"
msgstr ""
+msgid "Upload File"
+msgstr ""
+
msgid "Upload License"
msgstr ""
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index 265e2b7573c..048119d65cb 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -52,7 +52,7 @@ module QA
using_wait_time 0 do
set_initial_password_if_present
- raise NotImplementedError if Runtime::User.ldap_user? && user&.credentials_given?
+ raise 'If an LDAP user is provided, it must be used for sign-in', QA::Resource::User::InvalidUserError if Runtime::User.ldap_user? && user && user.username != Runtime::User.ldap_username
if Runtime::User.ldap_user?
sign_in_using_ldap_credentials(user: user || Runtime::User)
diff --git a/qa/qa/resource/personal_access_token.rb b/qa/qa/resource/personal_access_token.rb
index 463e780f89b..59ae8f4de7a 100644
--- a/qa/qa/resource/personal_access_token.rb
+++ b/qa/qa/resource/personal_access_token.rb
@@ -4,17 +4,59 @@ require 'date'
module QA
module Resource
- ##
- # Create a personal access token that can be used by the api
- #
class PersonalAccessToken < Base
attr_accessor :name
- attribute :access_token do
+ # The user for which the personal access token is to be created
+ # This *could* be different than the api_client.user or the api_user provided by the QA::Resource::ApiFabricator module
+ attr_writer :user
+
+ attribute :token do
Page::Profile::PersonalAccessTokens.perform(&:created_access_token)
end
+ # Only Admins can create PAT via the API.
+ # If Runtime::Env.admin_personal_access_token is provided, fabricate via the API,
+ # else, fabricate via the browser.
+ def fabricate_via_api!
+ if Runtime::Env.admin_personal_access_token && !@user.nil?
+ self.api_client = Runtime::API::Client.as_admin
+
+ super
+ else
+ fabricate!
+ end
+ end
+
+ # When a user is not provided, use default user
+ def user
+ @user || Resource::User.default
+ end
+
+ def api_post_path
+ "/users/#{user.api_resource[:id]}/personal_access_tokens"
+ end
+
+ def api_get_path
+ '/personal_access_tokens'
+ end
+
+ def api_post_body
+ {
+ name: name || 'api-test-token',
+ scopes: ["api"]
+ }
+ end
+
+ def resource_web_url(resource)
+ super
+ rescue ResourceURLMissingError
+ # this particular resource does not expose a web_url property
+ end
+
def fabricate!
+ Flow::Login.sign_in_unless_signed_in(as: user)
+
Page::Main::Menu.perform(&:click_edit_profile_link)
Page::Profile::Menu.perform(&:click_access_tokens)
diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb
index f95a68918dc..d1a310c7c43 100644
--- a/qa/qa/resource/user.rb
+++ b/qa/qa/resource/user.rb
@@ -5,6 +5,8 @@ require 'securerandom'
module QA
module Resource
class User < Base
+ InvalidUserError = Class.new(RuntimeError)
+
attr_reader :unique_id
attr_writer :username, :password
attr_accessor :admin, :provider, :extern_uid, :expect_fabrication_success
@@ -21,6 +23,13 @@ module QA
@expect_fabrication_success = true
end
+ def self.default
+ Resource::User.new.tap do |user|
+ user.username = Runtime::User.ldap_user? ? Runtime::User.ldap_username : Runtime::User.username
+ user.password = Runtime::User.ldap_user? ? Runtime::User.ldap_password : Runtime::User.password
+ end
+ end
+
def admin?
api_resource&.dig(:is_admin) || false
end
@@ -28,10 +37,12 @@ module QA
def username
@username || "qa-user-#{unique_id}"
end
+ alias_method :ldap_username, :username
def password
@password || 'password'
end
+ alias_method :ldap_password, :password
def name
@name ||= api_resource&.dig(:name) || "QA User #{unique_id}"
@@ -138,8 +149,8 @@ module QA
return {} unless extern_uid && provider
{
- extern_uid: extern_uid,
- provider: provider
+ extern_uid: extern_uid,
+ provider: provider
}
end
diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb
index e4de033c309..4126ff9ff5a 100644
--- a/qa/qa/runtime/api/client.rb
+++ b/qa/qa/runtime/api/client.rb
@@ -23,22 +23,30 @@ module QA
# unless a specific user has been passed
@user.nil? ? Runtime::Env.personal_access_token ||= create_personal_access_token : create_personal_access_token
end
+
+ if @user&.admin?
+ Runtime::Env.admin_personal_access_token = @personal_access_token
+ end
+
+ @personal_access_token
end
def self.as_admin
- if Runtime::Env.admin_personal_access_token
- Runtime::API::Client.new(:gitlab, personal_access_token: Runtime::Env.admin_personal_access_token)
- else
- user = Resource::User.fabricate_via_api! do |user|
- user.username = Runtime::User.admin_username
- user.password = Runtime::User.admin_password
+ @admin_client ||= begin
+ if Runtime::Env.admin_personal_access_token
+ Runtime::API::Client.new(:gitlab, personal_access_token: Runtime::Env.admin_personal_access_token)
+ else
+ user = Resource::User.fabricate_via_api! do |user|
+ user.username = Runtime::User.admin_username
+ user.password = Runtime::User.admin_password
+ end
+
+ unless user.admin?
+ raise AuthorizationError, "User '#{user.username}' is not an administrator."
+ end
+
+ Runtime::API::Client.new(:gitlab, user: user)
end
-
- unless user.admin?
- raise AuthorizationError, "User '#{user.username}' is not an administrator."
- end
-
- Runtime::API::Client.new(:gitlab, user: user)
end
end
@@ -67,9 +75,9 @@ module QA
Page::Main::Menu.perform(&:sign_out) if @is_new_session && signed_in_initially
- Flow::Login.sign_in_unless_signed_in(as: @user)
-
- token = Resource::PersonalAccessToken.fabricate!.access_token
+ token = Resource::PersonalAccessToken.fabricate! do |pat|
+ pat.user = user
+ end.token
# If this is a new session, that tests that follow could fail if they
# try to sign in without starting a new session.
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 6c4139da83f..7aa45204513 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -8,7 +8,7 @@ module QA
module Env
extend self
- attr_writer :personal_access_token
+ attr_writer :personal_access_token, :admin_personal_access_token
ENV_VARIABLES = Gitlab::QA::Runtime::Env::ENV_VARIABLES
@@ -78,18 +78,6 @@ module QA
ENV['QA_PRAEFECT_REPOSITORY_STORAGE']
end
- def admin_password
- ENV['GITLAB_ADMIN_PASSWORD']
- end
-
- def admin_username
- ENV['GITLAB_ADMIN_USERNAME']
- end
-
- def admin_personal_access_token
- ENV['GITLAB_QA_ADMIN_ACCESS_TOKEN']
- end
-
def ci_job_url
ENV['CI_JOB_URL']
end
@@ -140,6 +128,18 @@ module QA
enabled?(ENV['SIGNUP_DISABLED'], default: false)
end
+ def admin_password
+ ENV['GITLAB_ADMIN_PASSWORD']
+ end
+
+ def admin_username
+ ENV['GITLAB_ADMIN_USERNAME']
+ end
+
+ def admin_personal_access_token
+ @admin_personal_access_token ||= ENV['GITLAB_QA_ADMIN_ACCESS_TOKEN']
+ end
+
# specifies token that can be used for the api
def personal_access_token
@personal_access_token ||= ENV['GITLAB_QA_ACCESS_TOKEN']
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
index ffc2290b644..4141060b6cb 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
@@ -23,6 +23,7 @@ module QA
@personal_access_token = Runtime::Env.personal_access_token
Runtime::Env.personal_access_token = nil
+
ldap_username = Runtime::Env.ldap_username
Runtime::Env.ldap_username = nil
diff --git a/qa/qa/specs/features/browser_ui/3_create/jenkins/jenkins_build_status_spec.rb b/qa/qa/specs/features/browser_ui/3_create/jenkins/jenkins_build_status_spec.rb
index 0fec7bc9e9d..734ff160937 100644
--- a/qa/qa/specs/features/browser_ui/3_create/jenkins/jenkins_build_status_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/jenkins/jenkins_build_status_spec.rb
@@ -107,7 +107,7 @@ module QA
def fabricate_personal_access_token
login_to_gitlab
- token = Resource::PersonalAccessToken.fabricate!.access_token
+ token = Resource::PersonalAccessToken.fabricate!.token
Page::Main::Menu.perform(&:sign_out)
token
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
index 9edde7ac12f..fb9e42a6960 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Create' do
- describe 'Git clone over HTTP', :ldap_no_tls do
+ describe 'Git clone over HTTP' do
let(:project) do
Resource::Project.fabricate_via_api! do |scenario|
scenario.name = 'project-with-code'
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
index 35ec2135491..2c0fb5ea290 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
@@ -2,11 +2,11 @@
module QA
RSpec.describe 'Create' do
- describe 'Git push over HTTP', :ldap_no_tls, :smoke do
+ describe 'Git push over HTTP', :smoke do
it 'user using a personal access token pushes code to the repository', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1278' do
Flow::Login.sign_in
- access_token = Resource::PersonalAccessToken.fabricate!.access_token
+ access_token = Resource::PersonalAccessToken.fabricate!.token
user = Resource::User.new.tap do |user|
user.username = Runtime::User.username
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb
index 222eb3771ad..1423e3c45ce 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb
@@ -17,7 +17,7 @@ module QA
p.initialize_with_readme = true
end
- @api_client = Runtime::API::Client.new(:gitlab, personal_access_token: Runtime::Env.admin_personal_access_token)
+ @api_client = Runtime::API::Client.as_admin
end
after(:context) do
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
index 0857ae05167..861efa8b45a 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Create' do
- describe 'Git push over HTTP', :ldap_no_tls do
+ describe 'Git push over HTTP' do
it 'user pushes code to the repository', :smoke, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1702' do
Flow::Login.sign_in
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
index 2b249f779d9..ce7fdf379a4 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Create' do
- describe 'Protected branch support', :ldap_no_tls do
+ describe 'Protected branch support' do
let(:branch_name) { 'protected-branch' }
let(:commit_message) { 'Protected push commit message' }
let(:project) do
diff --git a/qa/qa/specs/features/browser_ui/5_package/maven_gradle_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/maven_gradle_repository_spec.rb
index 6ab4a957c57..b5b050a5dfe 100644
--- a/qa/qa/specs/features/browser_ui/5_package/maven_gradle_repository_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/maven_gradle_repository_spec.rb
@@ -13,7 +13,7 @@ module QA
Flow::Login.sign_in
end
- Resource::PersonalAccessToken.fabricate!.access_token
+ Resource::PersonalAccessToken.fabricate!.token
end
let(:project) do
diff --git a/qa/qa/specs/features/browser_ui/5_package/maven_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/maven_repository_spec.rb
index 1504f2be545..b6577dfd17a 100644
--- a/qa/qa/specs/features/browser_ui/5_package/maven_repository_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/maven_repository_spec.rb
@@ -15,7 +15,7 @@ module QA
Flow::Login.sign_in
end
- Resource::PersonalAccessToken.fabricate!.access_token
+ Resource::PersonalAccessToken.fabricate!.token
end
let(:project) do
diff --git a/qa/qa/specs/features/browser_ui/5_package/npm_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/npm_registry_spec.rb
index 0b1b6e649a1..97df8fedf87 100644
--- a/qa/qa/specs/features/browser_ui/5_package/npm_registry_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/npm_registry_spec.rb
@@ -12,7 +12,7 @@ module QA
Flow::Login.sign_in
end
- Resource::PersonalAccessToken.fabricate!.access_token
+ Resource::PersonalAccessToken.fabricate!.token
end
let(:project) do
diff --git a/qa/qa/tools/initialize_gitlab_auth.rb b/qa/qa/tools/initialize_gitlab_auth.rb
index b06ddcab040..3ead8fc9bd4 100644
--- a/qa/qa/tools/initialize_gitlab_auth.rb
+++ b/qa/qa/tools/initialize_gitlab_auth.rb
@@ -23,7 +23,7 @@ module QA
Flow::Login.sign_in
puts "Creating an API scoped access token for the root user..."
- puts "Token: #{Resource::PersonalAccessToken.fabricate!.access_token}"
+ puts "Token: #{Resource::PersonalAccessToken.fabricate!.token}"
end
end
end
diff --git a/qa/spec/runtime/api/client_spec.rb b/qa/spec/runtime/api/client_spec.rb
index dd139fda980..36ee563de39 100644
--- a/qa/spec/runtime/api/client_spec.rb
+++ b/qa/spec/runtime/api/client_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe QA::Runtime::API::Client do
end
it 'returns a created token' do
- client = described_class.new(user: { username: 'foo' })
+ client = described_class.new(user: Struct.new(:username, :admin?).new('foo', false))
expect(client).to receive(:create_personal_access_token).and_return('created_token')
diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb
index 3396ae6f0b8..5a98721466f 100644
--- a/qa/spec/runtime/env_spec.rb
+++ b/qa/spec/runtime/env_spec.rb
@@ -232,6 +232,7 @@ RSpec.describe QA::Runtime::Env do
describe '.require_admin_access_token!' do
it 'raises ArgumentError if GITLAB_QA_ADMIN_ACCESS_TOKEN is not specified' do
+ described_class.instance_variable_set(:@admin_personal_access_token, nil)
stub_env('GITLAB_QA_ADMIN_ACCESS_TOKEN', nil)
expect { described_class.require_admin_access_token! }.to raise_error(ArgumentError)
diff --git a/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb b/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb
index f664604ac15..e0f86876f67 100644
--- a/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb
+++ b/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb
@@ -37,13 +37,24 @@ RSpec.describe Projects::DesignManagement::Designs::RawImagesController do
# For security, .svg images should only ever be served with Content-Disposition: attachment.
# If this specs ever fails we must assess whether we should be serving svg images.
# See https://gitlab.com/gitlab-org/gitlab/issues/12771
- it 'serves files with `Content-Disposition: attachment`' do
+ it 'serves files with `Content-Disposition` header set to attachment plus the filename' do
subject
- expect(response.header['Content-Disposition']).to eq('attachment')
+ expect(response.header['Content-Disposition']).to match "attachment; filename=\"#{design.filename}\""
expect(response).to have_gitlab_http_status(:ok)
end
+ context 'when the feature flag attachment_with_filename is disabled' do
+ it 'serves files with just `attachment` in the disposition header' do
+ stub_feature_flags(attachment_with_filename: false)
+
+ subject
+
+ expect(response.header['Content-Disposition']).to eq('attachment')
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
it 'serves files with Workhorse' do
subject
diff --git a/spec/factories/gitlab/database/background_migration/batched_jobs.rb b/spec/factories/gitlab/database/background_migration/batched_jobs.rb
new file mode 100644
index 00000000000..52bc04447da
--- /dev/null
+++ b/spec/factories/gitlab/database/background_migration/batched_jobs.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :batched_background_migration_job, class: '::Gitlab::Database::BackgroundMigration::BatchedJob' do
+ batched_migration factory: :batched_background_migration
+
+ min_value { 1 }
+ max_value { 10 }
+ batch_size { 5 }
+ sub_batch_size { 1 }
+ end
+end
diff --git a/spec/factories/gitlab/database/background_migration/batched_migrations.rb b/spec/factories/gitlab/database/background_migration/batched_migrations.rb
new file mode 100644
index 00000000000..03ae2e664f0
--- /dev/null
+++ b/spec/factories/gitlab/database/background_migration/batched_migrations.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :batched_background_migration, class: '::Gitlab::Database::BackgroundMigration::BatchedMigration' do
+ max_value { 10 }
+ batch_size { 5 }
+ sub_batch_size { 1 }
+ interval { 2.minutes }
+ job_class_name { 'Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob' }
+ table_name { :events }
+ column_name { :id }
+ end
+end
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index d6e1b170dd3..a03aabf9e4f 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -352,6 +352,20 @@ describe('Api', () => {
});
});
+ describe('projectGroups', () => {
+ it('fetches a project group', async () => {
+ const options = { unused: 'option' };
+ const projectId = 1;
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/groups.json`;
+ mock.onGet(expectedUrl, { params: options }).reply(httpStatus.OK, {
+ name: 'test',
+ });
+
+ const { name } = await Api.projectGroups(projectId, options);
+ expect(name).toBe('test');
+ });
+ });
+
describe('projectUsers', () => {
it('fetches all users of a particular project', (done) => {
const query = 'dummy query';
diff --git a/spec/frontend/projects/details/upload_button_spec.js b/spec/frontend/projects/details/upload_button_spec.js
new file mode 100644
index 00000000000..d7308963088
--- /dev/null
+++ b/spec/frontend/projects/details/upload_button_spec.js
@@ -0,0 +1,54 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import UploadButton from '~/projects/details/upload_button.vue';
+import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
+
+const MODAL_ID = 'details-modal-upload-blob';
+
+describe('UploadButton', () => {
+ let wrapper;
+ let glModalDirective;
+
+ const createComponent = () => {
+ glModalDirective = jest.fn();
+
+ return shallowMount(UploadButton, {
+ directives: {
+ glModal: {
+ bind(_, { value }) {
+ glModalDirective(value);
+ },
+ },
+ },
+ });
+ };
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays an upload button', () => {
+ expect(wrapper.find(GlButton).exists()).toBe(true);
+ });
+
+ it('contains a modal', () => {
+ const modal = wrapper.find(UploadBlobModal);
+
+ expect(modal.exists()).toBe(true);
+ expect(modal.props('modalId')).toBe(MODAL_ID);
+ });
+
+ describe('when clickinig the upload file button', () => {
+ beforeEach(() => {
+ wrapper.find(GlButton).vm.$emit('click');
+ });
+
+ it('opens the modal', () => {
+ expect(glModalDirective).toHaveBeenCalledWith(MODAL_ID);
+ });
+ });
+});
diff --git a/spec/graphql/resolvers/branch_commit_resolver_spec.rb b/spec/graphql/resolvers/branch_commit_resolver_spec.rb
index 78d4959c3f9..9d085fd19a6 100644
--- a/spec/graphql/resolvers/branch_commit_resolver_spec.rb
+++ b/spec/graphql/resolvers/branch_commit_resolver_spec.rb
@@ -15,6 +15,10 @@ RSpec.describe Resolvers::BranchCommitResolver do
is_expected.to eq(repository.commits('master', limit: 1).last)
end
+ it 'sets project container' do
+ expect(commit.container).to eq(repository.project)
+ end
+
context 'when branch does not exist' do
let(:branch) { nil }
diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb
index ef6bc864eef..62bd953cce8 100644
--- a/spec/helpers/invite_members_helper_spec.rb
+++ b/spec/helpers/invite_members_helper_spec.rb
@@ -71,15 +71,7 @@ RSpec.describe InviteMembersHelper do
allow(helper).to receive(:current_user) { owner }
end
- it 'returns false' do
- allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_a) { false }
-
- expect(helper.directly_invite_members?).to eq false
- end
-
it 'returns true' do
- allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_a) { true }
-
expect(helper.directly_invite_members?).to eq true
end
end
@@ -90,8 +82,6 @@ RSpec.describe InviteMembersHelper do
end
it 'returns false' do
- allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_a) { true }
-
expect(helper.directly_invite_members?).to eq false
end
end
diff --git a/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb
index 110a1ff8a08..7ad93c3124a 100644
--- a/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb
+++ b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb
@@ -38,22 +38,9 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo
describe '#perform' do
let(:migration_class) { described_class.name }
- let!(:job1) do
- table(:background_migration_jobs).create!(
- class_name: migration_class,
- arguments: [1, 10, table_name, 'id', 'id', 'id_convert_to_bigint', sub_batch_size]
- )
- end
-
- let!(:job2) do
- table(:background_migration_jobs).create!(
- class_name: migration_class,
- arguments: [11, 20, table_name, 'id', 'id', 'id_convert_to_bigint', sub_batch_size]
- )
- end
it 'copies all primary keys in range' do
- subject.perform(12, 15, table_name, 'id', 'id', 'id_convert_to_bigint', sub_batch_size)
+ subject.perform(12, 15, table_name, 'id', sub_batch_size, 'id', 'id_convert_to_bigint')
expect(test_table.where('id = id_convert_to_bigint').pluck(:id)).to contain_exactly(12, 15)
expect(test_table.where(id_convert_to_bigint: 0).pluck(:id)).to contain_exactly(11, 19)
@@ -61,7 +48,7 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo
end
it 'copies all foreign keys in range' do
- subject.perform(10, 14, table_name, 'id', 'fk', 'fk_convert_to_bigint', sub_batch_size)
+ subject.perform(10, 14, table_name, 'id', sub_batch_size, 'fk', 'fk_convert_to_bigint')
expect(test_table.where('fk = fk_convert_to_bigint').pluck(:id)).to contain_exactly(11, 12)
expect(test_table.where(fk_convert_to_bigint: 0).pluck(:id)).to contain_exactly(15, 19)
@@ -71,21 +58,11 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo
it 'copies columns with NULLs' do
expect(test_table.where("name_convert_to_text = 'no name'").count).to eq(4)
- subject.perform(10, 20, table_name, 'id', 'name', 'name_convert_to_text', sub_batch_size)
+ subject.perform(10, 20, table_name, 'id', sub_batch_size, 'name', 'name_convert_to_text')
expect(test_table.where('name = name_convert_to_text').pluck(:id)).to contain_exactly(11, 12, 19)
expect(test_table.where('name is NULL and name_convert_to_text is NULL').pluck(:id)).to contain_exactly(15)
expect(test_table.where("name_convert_to_text = 'no name'").count).to eq(0)
end
-
- it 'tracks completion with BackgroundMigrationJob' do
- expect do
- subject.perform(11, 20, table_name, 'id', 'id', 'id_convert_to_bigint', sub_batch_size)
- end.to change { Gitlab::Database::BackgroundMigrationJob.succeeded.count }.from(0).to(1)
-
- expect(job1.reload.status).to eq(0)
- expect(job2.reload.status).to eq(1)
- expect(test_table.where('id = id_convert_to_bigint').count).to eq(4)
- end
end
end
diff --git a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
new file mode 100644
index 00000000000..1020aafcf08
--- /dev/null
+++ b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model do
+ it_behaves_like 'having unique enum values'
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:batched_migration).with_foreign_key(:batched_background_migration_id) }
+ end
+
+ describe 'delegated batched_migration attributes' do
+ let(:batched_job) { build(:batched_background_migration_job) }
+ let(:batched_migration) { batched_job.batched_migration }
+
+ describe '#migration_aborted?' do
+ before do
+ batched_migration.status = :aborted
+ end
+
+ it 'returns the migration aborted?' do
+ expect(batched_job.migration_aborted?).to eq(batched_migration.aborted?)
+ end
+ end
+
+ describe '#migration_job_class' do
+ it 'returns the migration job_class' do
+ expect(batched_job.migration_job_class).to eq(batched_migration.job_class)
+ end
+ end
+
+ describe '#migration_table_name' do
+ it 'returns the migration table_name' do
+ expect(batched_job.migration_table_name).to eq(batched_migration.table_name)
+ end
+ end
+
+ describe '#migration_column_name' do
+ it 'returns the migration column_name' do
+ expect(batched_job.migration_column_name).to eq(batched_migration.column_name)
+ end
+ end
+
+ describe '#migration_job_arguments' do
+ it 'returns the migration job_arguments' do
+ expect(batched_job.migration_job_arguments).to eq(batched_migration.job_arguments)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
new file mode 100644
index 00000000000..df0abaae721
--- /dev/null
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
@@ -0,0 +1,160 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :model do
+ it_behaves_like 'having unique enum values'
+
+ describe 'associations' do
+ it { is_expected.to have_many(:batched_jobs).with_foreign_key(:batched_background_migration_id) }
+
+ describe '#last_job' do
+ let!(:batched_migration) { create(:batched_background_migration) }
+ let!(:batched_job1) { create(:batched_background_migration_job, batched_migration: batched_migration) }
+ let!(:batched_job2) { create(:batched_background_migration_job, batched_migration: batched_migration) }
+
+ it 'returns the most recent (in order of id) batched job' do
+ expect(batched_migration.last_job).to eq(batched_job2)
+ end
+ end
+ end
+
+ describe '.queue_order' do
+ let!(:migration1) { create(:batched_background_migration) }
+ let!(:migration2) { create(:batched_background_migration) }
+ let!(:migration3) { create(:batched_background_migration) }
+
+ it 'returns batched migrations ordered by their id' do
+ expect(described_class.queue_order.all).to eq([migration1, migration2, migration3])
+ end
+ end
+
+ describe '#interval_elapsed?' do
+ context 'when the migration has no last_job' do
+ let(:batched_migration) { build(:batched_background_migration) }
+
+ it 'returns true' do
+ expect(batched_migration.interval_elapsed?).to eq(true)
+ end
+ end
+
+ context 'when the migration has a last_job' do
+ let(:interval) { 2.minutes }
+ let(:batched_migration) { create(:batched_background_migration, interval: interval) }
+
+ context 'when the last_job is less than an interval old' do
+ it 'returns false' do
+ freeze_time do
+ create(:batched_background_migration_job,
+ batched_migration: batched_migration,
+ created_at: Time.current - 1.minute)
+
+ expect(batched_migration.interval_elapsed?).to eq(false)
+ end
+ end
+ end
+
+ context 'when the last_job is exactly an interval old' do
+ it 'returns true' do
+ freeze_time do
+ create(:batched_background_migration_job,
+ batched_migration: batched_migration,
+ created_at: Time.current - 2.minutes)
+
+ expect(batched_migration.interval_elapsed?).to eq(true)
+ end
+ end
+ end
+
+ context 'when the last_job is more than an interval old' do
+ it 'returns true' do
+ freeze_time do
+ create(:batched_background_migration_job,
+ batched_migration: batched_migration,
+ created_at: Time.current - 3.minutes)
+
+ expect(batched_migration.interval_elapsed?).to eq(true)
+ end
+ end
+ end
+ end
+ end
+
+ describe '#create_batched_job!' do
+ let(:batched_migration) { create(:batched_background_migration) }
+
+ it 'creates a batched_job with the correct batch configuration' do
+ batched_job = batched_migration.create_batched_job!(1, 5)
+
+ expect(batched_job).to have_attributes(
+ min_value: 1,
+ max_value: 5,
+ batch_size: batched_migration.batch_size,
+ sub_batch_size: batched_migration.sub_batch_size)
+ end
+ end
+
+ describe '#next_min_value' do
+ let!(:batched_migration) { create(:batched_background_migration) }
+
+ context 'when a previous job exists' do
+ let!(:batched_job) { create(:batched_background_migration_job, batched_migration: batched_migration) }
+
+ it 'returns the next value after the previous maximum' do
+ expect(batched_migration.next_min_value).to eq(batched_job.max_value + 1)
+ end
+ end
+
+ context 'when a previous job does not exist' do
+ it 'returns the migration minimum value' do
+ expect(batched_migration.next_min_value).to eq(batched_migration.min_value)
+ end
+ end
+ end
+
+ describe '#job_class' do
+ let(:job_class) { Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob }
+ let(:batched_migration) { build(:batched_background_migration) }
+
+ it 'returns the class of the job for the migration' do
+ expect(batched_migration.job_class).to eq(job_class)
+ end
+ end
+
+ describe '#batch_class' do
+ let(:batch_class) { Gitlab::Database::BackgroundMigration::PrimaryKeyBatchingStrategy}
+ let(:batched_migration) { build(:batched_background_migration) }
+
+ it 'returns the class of the batch strategy for the migration' do
+ expect(batched_migration.batch_class).to eq(batch_class)
+ end
+ end
+
+ shared_examples_for 'an attr_writer that normalizes assigned class names' do |attribute_name|
+ let(:batched_migration) { build(:batched_background_migration) }
+
+ context 'when the toplevel namespace prefix exists' do
+ it 'removes the leading prefix' do
+ batched_migration.public_send(:"#{attribute_name}=", '::Foo::Bar')
+
+ expect(batched_migration[attribute_name]).to eq('Foo::Bar')
+ end
+ end
+
+ context 'when the toplevel namespace prefix does not exist' do
+ it 'does not change the given class name' do
+ batched_migration.public_send(:"#{attribute_name}=", '::Foo::Bar')
+
+ expect(batched_migration[attribute_name]).to eq('Foo::Bar')
+ end
+ end
+ end
+
+ describe '#job_class_name=' do
+ it_behaves_like 'an attr_writer that normalizes assigned class names', :job_class_name
+ end
+
+ describe '#batch_class_name=' do
+ it_behaves_like 'an attr_writer that normalizes assigned class names', :batch_class_name
+ end
+end
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
new file mode 100644
index 00000000000..17cceb35ff7
--- /dev/null
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '#perform' do
+ let(:migration_wrapper) { described_class.new }
+ let(:job_class) { Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob }
+
+ let_it_be(:active_migration) { create(:batched_background_migration, :active, job_arguments: [:id, :other_id]) }
+
+ let!(:job_record) { create(:batched_background_migration_job, batched_migration: active_migration) }
+
+ it 'runs the migration job' do
+ expect_next_instance_of(job_class) do |job_instance|
+ expect(job_instance).to receive(:perform).with(1, 10, 'events', 'id', 1, 'id', 'other_id')
+ end
+
+ migration_wrapper.perform(job_record)
+ end
+
+ it 'updates the the tracking record in the database' do
+ expect(job_record).to receive(:update!).with(hash_including(attempts: 1, status: :running)).and_call_original
+
+ freeze_time do
+ migration_wrapper.perform(job_record)
+
+ reloaded_job_record = job_record.reload
+
+ expect(reloaded_job_record).not_to be_pending
+ expect(reloaded_job_record.attempts).to eq(1)
+ expect(reloaded_job_record.started_at).to eq(Time.current)
+ end
+ end
+
+ context 'when the migration job does not raise an error' do
+ it 'marks the tracking record as succeeded' do
+ expect_next_instance_of(job_class) do |job_instance|
+ expect(job_instance).to receive(:perform).with(1, 10, 'events', 'id', 1, 'id', 'other_id')
+ end
+
+ freeze_time do
+ migration_wrapper.perform(job_record)
+
+ reloaded_job_record = job_record.reload
+
+ expect(reloaded_job_record).to be_succeeded
+ expect(reloaded_job_record.finished_at).to eq(Time.current)
+ end
+ end
+ end
+
+ context 'when the migration job raises an error' do
+ it 'marks the tracking record as failed before raising the error' do
+ expect_next_instance_of(job_class) do |job_instance|
+ expect(job_instance).to receive(:perform)
+ .with(1, 10, 'events', 'id', 1, 'id', 'other_id')
+ .and_raise(RuntimeError, 'Something broke!')
+ end
+
+ freeze_time do
+ expect { migration_wrapper.perform(job_record) }.to raise_error(RuntimeError, 'Something broke!')
+
+ reloaded_job_record = job_record.reload
+
+ expect(reloaded_job_record).to be_failed
+ expect(reloaded_job_record.finished_at).to eq(Time.current)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/background_migration/primary_key_batching_strategy_spec.rb b/spec/lib/gitlab/database/background_migration/primary_key_batching_strategy_spec.rb
new file mode 100644
index 00000000000..6baf6f319cf
--- /dev/null
+++ b/spec/lib/gitlab/database/background_migration/primary_key_batching_strategy_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::BackgroundMigration::PrimaryKeyBatchingStrategy, '#next_batch' do
+ let(:batching_strategy) { described_class.new }
+
+ let_it_be(:event1) { create(:event) }
+ let_it_be(:event2) { create(:event) }
+ let_it_be(:event3) { create(:event) }
+ let_it_be(:event4) { create(:event) }
+
+ context 'when starting on the first batch' do
+ it 'returns the bounds of the next batch' do
+ batch_bounds = batching_strategy.next_batch(:events, :id, batch_min_value: event1.id, batch_size: 3)
+
+ expect(batch_bounds).to eq([event1.id, event3.id])
+ end
+ end
+
+ context 'when additional batches remain' do
+ it 'returns the bounds of the next batch' do
+ batch_bounds = batching_strategy.next_batch(:events, :id, batch_min_value: event2.id, batch_size: 3)
+
+ expect(batch_bounds).to eq([event2.id, event4.id])
+ end
+ end
+
+ context 'when on the final batch' do
+ it 'returns the bounds of the next batch' do
+ batch_bounds = batching_strategy.next_batch(:events, :id, batch_min_value: event4.id, batch_size: 3)
+
+ expect(batch_bounds).to eq([event4.id, event4.id])
+ end
+ end
+
+ context 'when no additional batches remain' do
+ it 'returns nil' do
+ batch_bounds = batching_strategy.next_batch(:events, :id, batch_min_value: event4.id + 1, batch_size: 1)
+
+ expect(batch_bounds).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/background_migration/scheduler_spec.rb b/spec/lib/gitlab/database/background_migration/scheduler_spec.rb
new file mode 100644
index 00000000000..ba745acdf8a
--- /dev/null
+++ b/spec/lib/gitlab/database/background_migration/scheduler_spec.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::BackgroundMigration::Scheduler, '#perform' do
+ let(:scheduler) { described_class.new }
+
+ shared_examples_for 'it has no jobs to run' do
+ it 'does not create and run a migration job' do
+ test_wrapper = double('test wrapper')
+
+ expect(test_wrapper).not_to receive(:perform)
+
+ expect do
+ scheduler.perform(migration_wrapper: test_wrapper)
+ end.not_to change { Gitlab::Database::BackgroundMigration::BatchedJob.count }
+ end
+ end
+
+ context 'when there are no active migrations' do
+ let!(:migration) { create(:batched_background_migration, :finished) }
+
+ it_behaves_like 'it has no jobs to run'
+ end
+
+ shared_examples_for 'it has completed the migration' do
+ it 'marks the migration as finished' do
+ relation = Gitlab::Database::BackgroundMigration::BatchedMigration.finished.where(id: first_migration.id)
+
+ expect { scheduler.perform }.to change { relation.count }.by(1)
+ end
+ end
+
+ context 'when there are active migrations' do
+ let!(:first_migration) { create(:batched_background_migration, :active, batch_size: 2) }
+ let!(:last_migration) { create(:batched_background_migration, :active) }
+
+ let(:job_relation) do
+ Gitlab::Database::BackgroundMigration::BatchedJob.where(batched_background_migration_id: first_migration.id)
+ end
+
+ context 'when the migration interval has not elapsed' do
+ before do
+ expect_next_found_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigration) do |migration|
+ expect(migration).to receive(:interval_elapsed?).and_return(false)
+ end
+ end
+
+ it_behaves_like 'it has no jobs to run'
+ end
+
+ context 'when the interval has elapsed' do
+ before do
+ expect_next_found_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigration) do |migration|
+ expect(migration).to receive(:interval_elapsed?).and_return(true)
+ end
+ end
+
+ context 'when the first migration has no previous jobs' do
+ context 'when the migration has batches to process' do
+ let!(:event1) { create(:event) }
+ let!(:event2) { create(:event) }
+ let!(:event3) { create(:event) }
+
+ it 'runs the job for the first batch' do
+ first_migration.update!(min_value: event1.id, max_value: event3.id)
+
+ expect_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper) do |wrapper|
+ expect(wrapper).to receive(:perform).and_wrap_original do |_, job_record|
+ expect(job_record).to eq(job_relation.first)
+ end
+ end
+
+ expect { scheduler.perform }.to change { job_relation.count }.by(1)
+
+ expect(job_relation.first).to have_attributes(
+ min_value: event1.id,
+ max_value: event2.id,
+ batch_size: first_migration.batch_size,
+ sub_batch_size: first_migration.sub_batch_size)
+ end
+ end
+
+ context 'when the migration has no batches to process' do
+ it_behaves_like 'it has no jobs to run'
+ it_behaves_like 'it has completed the migration'
+ end
+ end
+
+ context 'when the first migration has previous jobs' do
+ let!(:event1) { create(:event) }
+ let!(:event2) { create(:event) }
+ let!(:event3) { create(:event) }
+
+ let!(:previous_job) do
+ create(:batched_background_migration_job,
+ batched_migration: first_migration,
+ min_value: event1.id,
+ max_value: event2.id,
+ batch_size: 2,
+ sub_batch_size: 1)
+ end
+
+ context 'when the migration is ready to process another job' do
+ it 'runs the migration job for the next batch' do
+ first_migration.update!(min_value: event1.id, max_value: event3.id)
+
+ expect_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper) do |wrapper|
+ expect(wrapper).to receive(:perform).and_wrap_original do |_, job_record|
+ expect(job_record).to eq(job_relation.last)
+ end
+ end
+
+ expect { scheduler.perform }.to change { job_relation.count }.by(1)
+
+ expect(job_relation.last).to have_attributes(
+ min_value: event3.id,
+ max_value: event3.id,
+ batch_size: first_migration.batch_size,
+ sub_batch_size: first_migration.sub_batch_size)
+ end
+ end
+
+ context 'when the migration has no batches remaining' do
+ let!(:final_job) do
+ create(:batched_background_migration_job,
+ batched_migration: first_migration,
+ min_value: event3.id,
+ max_value: event3.id,
+ batch_size: 2,
+ sub_batch_size: 1)
+ end
+
+ it_behaves_like 'it has no jobs to run'
+ it_behaves_like 'it has completed the migration'
+ end
+ end
+
+ context 'when the bounds of the next batch exceed the migration maximum value' do
+ let!(:events) { create_list(:event, 3) }
+ let(:event1) { events[0] }
+ let(:event2) { events[1] }
+
+ context 'when the batch maximum exceeds the migration maximum' do
+ it 'clamps the batch maximum to the migration maximum' do
+ first_migration.update!(batch_size: 5, min_value: event1.id, max_value: event2.id)
+
+ expect_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper) do |wrapper|
+ expect(wrapper).to receive(:perform)
+ end
+
+ expect { scheduler.perform }.to change { job_relation.count }.by(1)
+
+ expect(job_relation.first).to have_attributes(
+ min_value: event1.id,
+ max_value: event2.id,
+ batch_size: first_migration.batch_size,
+ sub_batch_size: first_migration.sub_batch_size)
+ end
+ end
+
+ context 'when the batch minimum exceeds the migration maximum' do
+ let!(:previous_job) do
+ create(:batched_background_migration_job,
+ batched_migration: first_migration,
+ min_value: event1.id,
+ max_value: event2.id,
+ batch_size: 5,
+ sub_batch_size: 1)
+ end
+
+ before do
+ first_migration.update!(batch_size: 5, min_value: 1, max_value: event2.id)
+ end
+
+ it_behaves_like 'it has no jobs to run'
+ it_behaves_like 'it has completed the migration'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 6de7fc3a50e..aceb2299cc0 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -1720,7 +1720,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
.with(
2.minutes,
'CopyColumnUsingBackgroundMigrationJob',
- [event.id, event.id, :events, :id, :id, 'id_convert_to_bigint', 100]
+ [event.id, event.id, :events, :id, 100, :id, 'id_convert_to_bigint']
)
expect(Gitlab::BackgroundMigration)
diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb
index 3df82924092..83c6b556fc6 100644
--- a/spec/lib/gitlab/experimentation_spec.rb
+++ b/spec/lib/gitlab/experimentation_spec.rb
@@ -8,7 +8,6 @@ RSpec.describe Gitlab::Experimentation::EXPERIMENTS do
it 'temporarily ensures we know what experiments exist for backwards compatibility' do
expected_experiment_keys = [
:upgrade_link_in_user_menu_a,
- :invite_members_version_a,
:invite_members_version_b,
:invite_members_empty_group_version_a,
:contact_sales_btn_in_app
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 60ff2478997..42add883d3f 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1131,22 +1131,28 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
context 'when commit status is retried' do
- before do
+ let!(:old_commit_status) do
create(:commit_status, pipeline: pipeline,
- stage: 'build',
- name: 'mac',
- stage_idx: 0,
- status: 'success')
-
- Ci::ProcessPipelineService
- .new(pipeline)
- .execute
+ stage: 'build',
+ name: 'mac',
+ stage_idx: 0,
+ status: 'success')
end
- it 'ignores the previous state' do
- expect(statuses).to eq([%w(build success),
- %w(test success),
- %w(deploy running)])
+ context 'when FF ci_remove_update_retried_from_process_pipeline is disabled' do
+ before do
+ stub_feature_flags(ci_remove_update_retried_from_process_pipeline: false)
+
+ Ci::ProcessPipelineService
+ .new(pipeline)
+ .execute
+ end
+
+ it 'ignores the previous state' do
+ expect(statuses).to eq([%w(build success),
+ %w(test success),
+ %w(deploy running)])
+ end
end
end
end
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index bec15b788c3..10fa15d468f 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -291,7 +291,7 @@ RSpec.describe API::CommitStatuses do
end
context 'when retrying a commit status' do
- before do
+ subject(:post_request) do
post api(post_url, developer),
params: { state: 'failed', name: 'test', ref: 'master' }
@@ -300,15 +300,45 @@ RSpec.describe API::CommitStatuses do
end
it 'correctly posts a new commit status' do
+ post_request
+
expect(response).to have_gitlab_http_status(:created)
expect(json_response['sha']).to eq(commit.id)
expect(json_response['status']).to eq('success')
end
- it 'retries a commit status', :sidekiq_might_not_need_inline do
- expect(CommitStatus.count).to eq 2
- expect(CommitStatus.first).to be_retried
- expect(CommitStatus.last.pipeline).to be_success
+ context 'feature flags' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:ci_fix_commit_status_retried, :ci_remove_update_retried_from_process_pipeline, :previous_statuses_retried) do
+ true | true | true
+ true | false | true
+ false | true | false
+ false | false | true
+ end
+
+ with_them do
+ before do
+ stub_feature_flags(
+ ci_fix_commit_status_retried: ci_fix_commit_status_retried,
+ ci_remove_update_retried_from_process_pipeline: ci_remove_update_retried_from_process_pipeline
+ )
+ end
+
+ it 'retries a commit status', :sidekiq_might_not_need_inline do
+ post_request
+
+ expect(CommitStatus.count).to eq 2
+
+ if previous_statuses_retried
+ expect(CommitStatus.first).to be_retried
+ expect(CommitStatus.last.pipeline).to be_success
+ else
+ expect(CommitStatus.first).not_to be_retried
+ expect(CommitStatus.last.pipeline).to be_failed
+ end
+ end
+ end
end
end
diff --git a/spec/requests/projects/merge_requests/content_spec.rb b/spec/requests/projects/merge_requests/content_spec.rb
new file mode 100644
index 00000000000..7e5ec6f64c4
--- /dev/null
+++ b/spec/requests/projects/merge_requests/content_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'merge request content spec' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:merge_request) { create(:merge_request, :with_head_pipeline, target_project: project, source_project: project) }
+ let_it_be(:ci_build) { create(:ci_build, :artifacts, pipeline: merge_request.head_pipeline) }
+
+ before do
+ sign_in(user)
+ project.add_maintainer(user)
+ end
+
+ shared_examples 'cached widget request' do
+ it 'avoids N+1 queries when multiple job artifacts are present' do
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get cached_widget_project_json_merge_request_path(project, merge_request, format: :json)
+ end
+
+ create_list(:ci_build, 10, :artifacts, pipeline: merge_request.head_pipeline)
+
+ expect do
+ get cached_widget_project_json_merge_request_path(project, merge_request, format: :json)
+ end.not_to exceed_query_limit(control)
+ end
+ end
+
+ describe 'GET cached_widget' do
+ it_behaves_like 'cached widget request'
+
+ context 'with non_public_artifacts disabled' do
+ before do
+ stub_feature_flags(non_public_artifacts: false)
+ end
+
+ it_behaves_like 'cached widget request'
+ end
+ end
+end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index d316c9a262b..e02536fd07f 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -43,42 +43,59 @@ RSpec.describe Ci::ProcessPipelineService do
let!(:build) { create_build('build') }
let!(:test) { create_build('test') }
- it 'returns unique statuses' do
- subject.execute
+ context 'when FF ci_remove_update_retried_from_process_pipeline is enabled' do
+ it 'does not update older builds as retried' do
+ subject.execute
- expect(all_builds.latest).to contain_exactly(build, test)
- expect(all_builds.retried).to contain_exactly(build_retried)
+ expect(all_builds.latest).to contain_exactly(build, build_retried, test)
+ expect(all_builds.retried).to be_empty
+ end
end
- context 'counter ci_legacy_update_jobs_as_retried_total' do
- let(:counter) { double(increment: true) }
-
+ context 'when FF ci_remove_update_retried_from_process_pipeline is disabled' do
before do
- allow(Gitlab::Metrics).to receive(:counter).and_call_original
- allow(Gitlab::Metrics).to receive(:counter)
- .with(:ci_legacy_update_jobs_as_retried_total, anything)
- .and_return(counter)
+ stub_feature_flags(ci_remove_update_retried_from_process_pipeline: false)
end
- it 'increments the counter' do
- expect(counter).to receive(:increment)
-
+ it 'returns unique statuses' do
subject.execute
+
+ expect(all_builds.latest).to contain_exactly(build, test)
+ expect(all_builds.retried).to contain_exactly(build_retried)
end
- context 'when the previous build has already retried column true' do
+ context 'counter ci_legacy_update_jobs_as_retried_total' do
+ let(:counter) { double(increment: true) }
+
before do
- build_retried.update_columns(retried: true)
+ allow(Gitlab::Metrics).to receive(:counter).and_call_original
+ allow(Gitlab::Metrics).to receive(:counter)
+ .with(:ci_legacy_update_jobs_as_retried_total, anything)
+ .and_return(counter)
end
- it 'does not increment the counter' do
- expect(counter).not_to receive(:increment)
+ it 'increments the counter' do
+ expect(counter).to receive(:increment)
subject.execute
end
+
+ context 'when the previous build has already retried column true' do
+ before do
+ build_retried.update_columns(retried: true)
+ end
+
+ it 'does not increment the counter' do
+ expect(counter).not_to receive(:increment)
+
+ subject.execute
+ end
+ end
end
end
+ private
+
def create_build(name, **opts)
create(:ci_build, :created, pipeline: pipeline, name: name, **opts)
end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 6fa3d6efbb5..5410e784cc0 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -85,357 +85,343 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
end
end
- shared_examples 'project destroy' do
- it_behaves_like 'deleting the project'
+ it_behaves_like 'deleting the project'
- it 'invalidates personal_project_count cache' do
- expect(user).to receive(:invalidate_personal_projects_count)
+ it 'invalidates personal_project_count cache' do
+ expect(user).to receive(:invalidate_personal_projects_count)
- destroy_project(project, user, {})
+ destroy_project(project, user, {})
+ end
+
+ it 'performs cancel for project ci pipelines' do
+ expect(::Ci::AbortProjectPipelinesService).to receive_message_chain(:new, :execute).with(project)
+
+ destroy_project(project, user, {})
+ end
+
+ context 'when project has remote mirrors' do
+ let!(:project) do
+ create(:project, :repository, namespace: user.namespace).tap do |project|
+ project.remote_mirrors.create!(url: 'http://test.com')
+ end
end
- it 'performs cancel for project ci pipelines' do
- expect(::Ci::AbortProjectPipelinesService).to receive_message_chain(:new, :execute).with(project)
+ it 'destroys them' do
+ expect(RemoteMirror.count).to eq(1)
destroy_project(project, user, {})
+
+ expect(RemoteMirror.count).to eq(0)
end
+ end
- context 'when project has remote mirrors' do
- let!(:project) do
- create(:project, :repository, namespace: user.namespace).tap do |project|
- project.remote_mirrors.create!(url: 'http://test.com')
- end
+ context 'when project has exports' do
+ let!(:project_with_export) do
+ create(:project, :repository, namespace: user.namespace).tap do |project|
+ create(:import_export_upload,
+ project: project,
+ export_file: fixture_file_upload('spec/fixtures/project_export.tar.gz'))
end
+ end
- it 'destroys them' do
- expect(RemoteMirror.count).to eq(1)
-
- destroy_project(project, user, {})
+ it 'destroys project and export' do
+ expect do
+ destroy_project(project_with_export, user, {})
+ end.to change(ImportExportUpload, :count).by(-1)
- expect(RemoteMirror.count).to eq(0)
- end
+ expect(Project.all).not_to include(project_with_export)
end
+ end
- context 'when project has exports' do
- let!(:project_with_export) do
- create(:project, :repository, namespace: user.namespace).tap do |project|
- create(:import_export_upload,
- project: project,
- export_file: fixture_file_upload('spec/fixtures/project_export.tar.gz'))
- end
- end
+ context 'Sidekiq fake' do
+ before do
+ # Dont run sidekiq to check if renamed repository exists
+ Sidekiq::Testing.fake! { destroy_project(project, user, {}) }
+ end
- it 'destroys project and export' do
- expect do
- destroy_project(project_with_export, user, {})
- end.to change(ImportExportUpload, :count).by(-1)
+ it { expect(Project.all).not_to include(project) }
- expect(Project.all).not_to include(project_with_export)
- end
+ it do
+ expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey
end
- context 'Sidekiq fake' do
- before do
- # Dont run sidekiq to check if renamed repository exists
- Sidekiq::Testing.fake! { destroy_project(project, user, {}) }
- end
+ it do
+ expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_truthy
+ end
+ end
- it { expect(Project.all).not_to include(project) }
+ context 'when flushing caches fail due to Git errors' do
+ before do
+ allow(project.repository).to receive(:before_delete).and_raise(::Gitlab::Git::CommandError)
+ allow(Gitlab::GitLogger).to receive(:warn).with(
+ class: Repositories::DestroyService.name,
+ container_id: project.id,
+ disk_path: project.disk_path,
+ message: 'Gitlab::Git::CommandError').and_call_original
+ end
- it do
- expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey
- end
+ it_behaves_like 'deleting the project'
+ end
- it do
- expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_truthy
- end
+ context 'when flushing caches fail due to Redis' do
+ before do
+ new_user = create(:user)
+ project.team.add_user(new_user, Gitlab::Access::DEVELOPER)
+ allow_any_instance_of(described_class).to receive(:flush_caches).and_raise(::Redis::CannotConnectError)
end
- context 'when flushing caches fail due to Git errors' do
- before do
- allow(project.repository).to receive(:before_delete).and_raise(::Gitlab::Git::CommandError)
- allow(Gitlab::GitLogger).to receive(:warn).with(
- class: Repositories::DestroyService.name,
- container_id: project.id,
- disk_path: project.disk_path,
- message: 'Gitlab::Git::CommandError').and_call_original
+ it 'keeps project team intact upon an error' do
+ perform_enqueued_jobs do
+ destroy_project(project, user, {})
+ rescue ::Redis::CannotConnectError
end
- it_behaves_like 'deleting the project'
+ expect(project.team.members.count).to eq 2
end
+ end
- context 'when flushing caches fail due to Redis' do
+ context 'with async_execute', :sidekiq_inline do
+ let(:async) { true }
+
+ context 'async delete of project with private issue visibility' do
before do
- new_user = create(:user)
- project.team.add_user(new_user, Gitlab::Access::DEVELOPER)
- allow_any_instance_of(described_class).to receive(:flush_caches).and_raise(::Redis::CannotConnectError)
+ project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE)
end
- it 'keeps project team intact upon an error' do
- perform_enqueued_jobs do
- destroy_project(project, user, {})
- rescue ::Redis::CannotConnectError
- end
-
- expect(project.team.members.count).to eq 2
- end
+ it_behaves_like 'deleting the project'
end
- context 'with async_execute', :sidekiq_inline do
- let(:async) { true }
+ it_behaves_like 'deleting the project with pipeline and build'
- context 'async delete of project with private issue visibility' do
+ context 'errors' do
+ context 'when `remove_legacy_registry_tags` fails' do
before do
- project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE)
+ expect_any_instance_of(described_class)
+ .to receive(:remove_legacy_registry_tags).and_return(false)
end
- it_behaves_like 'deleting the project'
+ it_behaves_like 'handles errors thrown during async destroy', "Failed to remove some tags"
end
- it_behaves_like 'deleting the project with pipeline and build'
-
- context 'errors' do
- context 'when `remove_legacy_registry_tags` fails' do
- before do
- expect_any_instance_of(described_class)
- .to receive(:remove_legacy_registry_tags).and_return(false)
- end
-
- it_behaves_like 'handles errors thrown during async destroy', "Failed to remove some tags"
+ context 'when `remove_repository` fails' do
+ before do
+ expect_any_instance_of(described_class)
+ .to receive(:remove_repository).and_return(false)
end
- context 'when `remove_repository` fails' do
- before do
- expect_any_instance_of(described_class)
- .to receive(:remove_repository).and_return(false)
- end
+ it_behaves_like 'handles errors thrown during async destroy', "Failed to remove project repository"
+ end
- it_behaves_like 'handles errors thrown during async destroy', "Failed to remove project repository"
+ context 'when `execute` raises expected error' do
+ before do
+ expect_any_instance_of(Project)
+ .to receive(:destroy!).and_raise(StandardError.new("Other error message"))
end
- context 'when `execute` raises expected error' do
- before do
- expect_any_instance_of(Project)
- .to receive(:destroy!).and_raise(StandardError.new("Other error message"))
- end
+ it_behaves_like 'handles errors thrown during async destroy', "Other error message"
+ end
- it_behaves_like 'handles errors thrown during async destroy', "Other error message"
+ context 'when `execute` raises unexpected error' do
+ before do
+ expect_any_instance_of(Project)
+ .to receive(:destroy!).and_raise(Exception.new('Other error message'))
end
- context 'when `execute` raises unexpected error' do
- before do
- expect_any_instance_of(Project)
- .to receive(:destroy!).and_raise(Exception.new('Other error message'))
- end
-
- it 'allows error to bubble up and rolls back project deletion' do
- expect do
- destroy_project(project, user, {})
- end.to raise_error(Exception, 'Other error message')
+ it 'allows error to bubble up and rolls back project deletion' do
+ expect do
+ destroy_project(project, user, {})
+ end.to raise_error(Exception, 'Other error message')
- expect(project.reload.pending_delete).to be(false)
- expect(project.delete_error).to include("Other error message")
- end
+ expect(project.reload.pending_delete).to be(false)
+ expect(project.delete_error).to include("Other error message")
end
end
end
+ end
- describe 'container registry' do
- context 'when there are regular container repositories' do
- let(:container_repository) { create(:container_repository) }
+ describe 'container registry' do
+ context 'when there are regular container repositories' do
+ let(:container_repository) { create(:container_repository) }
- before do
- stub_container_registry_tags(repository: project.full_path + '/image',
- tags: ['tag'])
- project.container_repositories << container_repository
- end
+ before do
+ stub_container_registry_tags(repository: project.full_path + '/image',
+ tags: ['tag'])
+ project.container_repositories << container_repository
+ end
- context 'when image repository deletion succeeds' do
- it 'removes tags' do
- expect_any_instance_of(ContainerRepository)
- .to receive(:delete_tags!).and_return(true)
+ context 'when image repository deletion succeeds' do
+ it 'removes tags' do
+ expect_any_instance_of(ContainerRepository)
+ .to receive(:delete_tags!).and_return(true)
- destroy_project(project, user)
- end
+ destroy_project(project, user)
end
+ end
- context 'when image repository deletion fails' do
- it 'raises an exception' do
- expect_any_instance_of(ContainerRepository)
- .to receive(:delete_tags!).and_raise(RuntimeError)
+ context 'when image repository deletion fails' do
+ it 'raises an exception' do
+ expect_any_instance_of(ContainerRepository)
+ .to receive(:delete_tags!).and_raise(RuntimeError)
- expect(destroy_project(project, user)).to be false
- end
+ expect(destroy_project(project, user)).to be false
end
+ end
- context 'when registry is disabled' do
- before do
- stub_container_registry_config(enabled: false)
- end
+ context 'when registry is disabled' do
+ before do
+ stub_container_registry_config(enabled: false)
+ end
- it 'does not attempting to remove any tags' do
- expect(Projects::ContainerRepository::DestroyService).not_to receive(:new)
+ it 'does not attempting to remove any tags' do
+ expect(Projects::ContainerRepository::DestroyService).not_to receive(:new)
- destroy_project(project, user)
- end
+ destroy_project(project, user)
end
end
+ end
- context 'when there are tags for legacy root repository' do
- before do
- stub_container_registry_tags(repository: project.full_path,
- tags: ['tag'])
- end
+ context 'when there are tags for legacy root repository' do
+ before do
+ stub_container_registry_tags(repository: project.full_path,
+ tags: ['tag'])
+ end
- context 'when image repository tags deletion succeeds' do
- it 'removes tags' do
- expect_any_instance_of(ContainerRepository)
- .to receive(:delete_tags!).and_return(true)
+ context 'when image repository tags deletion succeeds' do
+ it 'removes tags' do
+ expect_any_instance_of(ContainerRepository)
+ .to receive(:delete_tags!).and_return(true)
- destroy_project(project, user)
- end
+ destroy_project(project, user)
end
+ end
- context 'when image repository tags deletion fails' do
- it 'raises an exception' do
- expect_any_instance_of(ContainerRepository)
- .to receive(:delete_tags!).and_return(false)
+ context 'when image repository tags deletion fails' do
+ it 'raises an exception' do
+ expect_any_instance_of(ContainerRepository)
+ .to receive(:delete_tags!).and_return(false)
- expect(destroy_project(project, user)).to be false
- end
+ expect(destroy_project(project, user)).to be false
end
end
end
+ end
- context 'for a forked project with LFS objects' do
- let(:forked_project) { fork_project(project, user) }
+ context 'for a forked project with LFS objects' do
+ let(:forked_project) { fork_project(project, user) }
- before do
- project.lfs_objects << create(:lfs_object)
- forked_project.reload
- end
+ before do
+ project.lfs_objects << create(:lfs_object)
+ forked_project.reload
+ end
- it 'destroys the fork' do
- expect { destroy_project(forked_project, user) }
- .not_to raise_error
- end
+ it 'destroys the fork' do
+ expect { destroy_project(forked_project, user) }
+ .not_to raise_error
end
+ end
- context 'as the root of a fork network' do
- let!(:fork_1) { fork_project(project, user) }
- let!(:fork_2) { fork_project(project, user) }
+ context 'as the root of a fork network' do
+ let!(:fork_1) { fork_project(project, user) }
+ let!(:fork_2) { fork_project(project, user) }
- it 'updates the fork network with the project name' do
- fork_network = project.fork_network
+ it 'updates the fork network with the project name' do
+ fork_network = project.fork_network
- destroy_project(project, user)
+ destroy_project(project, user)
- fork_network.reload
+ fork_network.reload
- expect(fork_network.deleted_root_project_name).to eq(project.full_name)
- expect(fork_network.root_project).to be_nil
- end
+ expect(fork_network.deleted_root_project_name).to eq(project.full_name)
+ expect(fork_network.root_project).to be_nil
end
+ end
- context 'repository +deleted path removal' do
- context 'regular phase' do
- it 'schedules +deleted removal of existing repos' do
- service = described_class.new(project, user, {})
- allow(service).to receive(:schedule_stale_repos_removal)
+ context 'repository +deleted path removal' do
+ context 'regular phase' do
+ it 'schedules +deleted removal of existing repos' do
+ service = described_class.new(project, user, {})
+ allow(service).to receive(:schedule_stale_repos_removal)
- expect(Repositories::ShellDestroyService).to receive(:new).and_call_original
- expect(GitlabShellWorker).to receive(:perform_in)
- .with(5.minutes, :remove_repository, project.repository_storage, removal_path(project.disk_path))
+ expect(Repositories::ShellDestroyService).to receive(:new).and_call_original
+ expect(GitlabShellWorker).to receive(:perform_in)
+ .with(5.minutes, :remove_repository, project.repository_storage, removal_path(project.disk_path))
- service.execute
- end
+ service.execute
end
+ end
- context 'stale cleanup' do
- let(:async) { true }
+ context 'stale cleanup' do
+ let(:async) { true }
- it 'schedules +deleted wiki and repo removal' do
- allow(ProjectDestroyWorker).to receive(:perform_async)
+ it 'schedules +deleted wiki and repo removal' do
+ allow(ProjectDestroyWorker).to receive(:perform_async)
- expect(Repositories::ShellDestroyService).to receive(:new).with(project.repository).and_call_original
- expect(GitlabShellWorker).to receive(:perform_in)
- .with(10.minutes, :remove_repository, project.repository_storage, removal_path(project.disk_path))
+ expect(Repositories::ShellDestroyService).to receive(:new).with(project.repository).and_call_original
+ expect(GitlabShellWorker).to receive(:perform_in)
+ .with(10.minutes, :remove_repository, project.repository_storage, removal_path(project.disk_path))
- expect(Repositories::ShellDestroyService).to receive(:new).with(project.wiki.repository).and_call_original
- expect(GitlabShellWorker).to receive(:perform_in)
- .with(10.minutes, :remove_repository, project.repository_storage, removal_path(project.wiki.disk_path))
+ expect(Repositories::ShellDestroyService).to receive(:new).with(project.wiki.repository).and_call_original
+ expect(GitlabShellWorker).to receive(:perform_in)
+ .with(10.minutes, :remove_repository, project.repository_storage, removal_path(project.wiki.disk_path))
- destroy_project(project, user, {})
- end
+ destroy_project(project, user, {})
end
end
+ end
- context 'snippets' do
- let!(:snippet1) { create(:project_snippet, project: project, author: user) }
- let!(:snippet2) { create(:project_snippet, project: project, author: user) }
-
- it 'does not include snippets when deleting in batches' do
- expect(project).to receive(:destroy_dependent_associations_in_batches).with({ exclude: [:container_repositories, :snippets] })
+ context 'snippets' do
+ let!(:snippet1) { create(:project_snippet, project: project, author: user) }
+ let!(:snippet2) { create(:project_snippet, project: project, author: user) }
- destroy_project(project, user)
- end
+ it 'does not include snippets when deleting in batches' do
+ expect(project).to receive(:destroy_dependent_associations_in_batches).with({ exclude: [:container_repositories, :snippets] })
- it 'calls the bulk snippet destroy service' do
- expect(project.snippets.count).to eq 2
-
- expect(Snippets::BulkDestroyService).to receive(:new)
- .with(user, project.snippets).and_call_original
+ destroy_project(project, user)
+ end
- expect do
- destroy_project(project, user)
- end.to change(Snippet, :count).by(-2)
- end
+ it 'calls the bulk snippet destroy service' do
+ expect(project.snippets.count).to eq 2
- context 'when an error is raised deleting snippets' do
- it 'does not delete project' do
- allow_next_instance_of(Snippets::BulkDestroyService) do |instance|
- allow(instance).to receive(:execute).and_return(ServiceResponse.error(message: 'foo'))
- end
+ expect(Snippets::BulkDestroyService).to receive(:new)
+ .with(user, project.snippets).and_call_original
- expect(destroy_project(project, user)).to be_falsey
- expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_truthy
- end
- end
+ expect do
+ destroy_project(project, user)
+ end.to change(Snippet, :count).by(-2)
end
- context 'error while destroying', :sidekiq_inline do
- let!(:pipeline) { create(:ci_pipeline, project: project) }
- let!(:builds) { create_list(:ci_build, 2, :artifacts, pipeline: pipeline) }
- let!(:build_trace) { create(:ci_build_trace_chunk, build: builds[0]) }
-
- it 'deletes on retry' do
- # We can expect this to timeout for very large projects
- # TODO: remove allow_next_instance_of: https://gitlab.com/gitlab-org/gitlab/-/issues/220440
- allow_any_instance_of(Ci::Build).to receive(:destroy).and_raise('boom')
- destroy_project(project, user, {})
-
- allow_any_instance_of(Ci::Build).to receive(:destroy).and_call_original
- destroy_project(project, user, {})
+ context 'when an error is raised deleting snippets' do
+ it 'does not delete project' do
+ allow_next_instance_of(Snippets::BulkDestroyService) do |instance|
+ allow(instance).to receive(:execute).and_return(ServiceResponse.error(message: 'foo'))
+ end
- expect(Project.unscoped.all).not_to include(project)
- expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey
- expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_falsey
- expect(project.all_pipelines).to be_empty
- expect(project.builds).to be_empty
+ expect(destroy_project(project, user)).to be_falsey
+ expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_truthy
end
end
end
- context 'when project_transactionless_destroy enabled' do
- it_behaves_like 'project destroy'
- end
+ context 'error while destroying', :sidekiq_inline do
+ let!(:pipeline) { create(:ci_pipeline, project: project) }
+ let!(:builds) { create_list(:ci_build, 2, :artifacts, pipeline: pipeline) }
+ let!(:build_trace) { create(:ci_build_trace_chunk, build: builds[0]) }
- context 'when project_transactionless_destroy disabled', :sidekiq_inline do
- before do
- stub_feature_flags(project_transactionless_destroy: false)
- end
+ it 'deletes on retry' do
+ # We can expect this to timeout for very large projects
+ # TODO: remove allow_next_instance_of: https://gitlab.com/gitlab-org/gitlab/-/issues/220440
+ allow_any_instance_of(Ci::Build).to receive(:destroy).and_raise('boom')
+ destroy_project(project, user, {})
+
+ allow_any_instance_of(Ci::Build).to receive(:destroy).and_call_original
+ destroy_project(project, user, {})
- it_behaves_like 'project destroy'
+ expect(Project.unscoped.all).not_to include(project)
+ expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey
+ expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_falsey
+ expect(project.all_pipelines).to be_empty
+ expect(project.builds).to be_empty
+ end
end
def destroy_project(project, user, params = {})
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index 6bf2876f640..b735f4b6bc2 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -335,6 +335,41 @@ RSpec.describe Projects::UpdatePagesService do
end
end
+ context 'when retrying the job' do
+ let!(:older_deploy_job) do
+ create(:generic_commit_status, :failed, pipeline: pipeline,
+ ref: build.ref,
+ stage: 'deploy',
+ name: 'pages:deploy')
+ end
+
+ before do
+ create(:ci_job_artifact, :correct_checksum, file: file, job: build)
+ create(:ci_job_artifact, file_type: :metadata, file_format: :gzip, file: metadata, job: build)
+ build.reload
+ end
+
+ it 'marks older pages:deploy jobs retried' do
+ expect(execute).to eq(:success)
+
+ expect(older_deploy_job.reload).to be_retried
+ end
+
+ context 'when FF ci_fix_commit_status_retried is disabled' do
+ before do
+ stub_feature_flags(ci_fix_commit_status_retried: false)
+ end
+
+ it 'does not mark older pages:deploy jobs retried' do
+ expect(execute).to eq(:success)
+
+ expect(older_deploy_job.reload).not_to be_retried
+ end
+ end
+ end
+
+ private
+
def deploy_status
GenericCommitStatus.find_by(name: 'pages:deploy')
end
diff --git a/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb b/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb
index c3e8f807afb..62aaec85162 100644
--- a/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb
@@ -17,6 +17,38 @@ RSpec.shared_examples 'raw snippet blob' do
end
end
+ context 'Content Disposition' do
+ context 'when the disposition is inline' do
+ let(:inline) { true }
+
+ it 'returns inline in the content disposition header' do
+ subject
+
+ expect(response.header['Content-Disposition']).to eq('inline')
+ end
+ end
+
+ context 'when the disposition is attachment' do
+ let(:inline) { false }
+
+ it 'returns attachment plus the filename in the content disposition header' do
+ subject
+
+ expect(response.header['Content-Disposition']).to match "attachment; filename=\"#{filepath}\""
+ end
+
+ context 'when the feature flag attachment_with_filename is disabled' do
+ it 'returns just attachment in the disposition header' do
+ stub_feature_flags(attachment_with_filename: false)
+
+ subject
+
+ expect(response.header['Content-Disposition']).to eq 'attachment'
+ end
+ end
+ end
+ end
+
context 'with invalid file path' do
let(:filepath) { 'doesnotexist' }
diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
index 3fec1a56c0c..7a32f61d4fa 100644
--- a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
+++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
@@ -1,11 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'issuable invite members experiments' do
- context 'when invite_members_version_a experiment is enabled' do
- before do
- stub_experiment_for_subject(invite_members_version_a: true)
- end
-
+ context 'when a privileged user can invite' do
it 'shows a link for inviting members and follows through to the members page' do
project.add_maintainer(user)
visit issuable_path
@@ -51,9 +47,9 @@ RSpec.shared_examples 'issuable invite members experiments' do
end
end
- context 'when no invite members experiments are enabled' do
+ context 'when invite_members_version_b experiment is disabled' do
it 'shows author in assignee dropdown and no invite link' do
- project.add_maintainer(user)
+ project.add_developer(user)
visit issuable_path
find('.block.assignee .edit-link').click