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:
-rw-r--r--app/assets/javascripts/issues/list/constants.js4
-rw-r--r--app/assets/javascripts/issues/list/queries/issue.fragment.graphql1
-rw-r--r--app/assets/javascripts/issues/list/utils.js20
-rw-r--r--app/assets/javascripts/projects/commit_box/info/init_details_button.js8
-rw-r--r--app/assets/javascripts/repository/commits_service.js2
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue47
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js1
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue19
-rw-r--r--app/assets/stylesheets/pages/issuable.scss6
-rw-r--r--app/controllers/graphql_controller.rb7
-rw-r--r--app/controllers/oauth/applications_controller.rb6
-rw-r--r--app/events/pages/page_deployed_event.rb17
-rw-r--r--app/helpers/sorting_helper.rb7
-rw-r--r--app/models/ci/build.rb5
-rw-r--r--app/models/ci/runner.rb1
-rw-r--r--app/models/commit_status.rb4
-rw-r--r--app/models/concerns/ci/bulk_insertable_tags.rb24
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/presenters/blob_presenter.rb2
-rw-r--r--app/services/ci/runners/register_runner_service.rb14
-rw-r--r--app/services/projects/update_pages_service.rb11
-rw-r--r--app/views/projects/find_file/show.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml7
-rw-r--r--app/views/shared/doorkeeper/applications/_index.html.haml28
-rw-r--r--data/removals/15_0/15-0-configure-self-managed-cert-based-kube-feature-flag.yml24
-rw-r--r--db/migrate/20220621202616_add_partial_index_on_oauth_access_tokens_revoked_at.rb19
-rw-r--r--db/post_migrate/20220621040800_backfill_imported_issue_search_data.rb26
-rw-r--r--db/post_migrate/20220628012902_finalise_project_namespace_members.rb22
-rw-r--r--db/schema_migrations/202206210408001
-rw-r--r--db/schema_migrations/202206212026161
-rw-r--r--db/schema_migrations/202206280129021
-rw-r--r--db/structure.sql4
-rw-r--r--doc/api/graphql/reference/index.md55
-rw-r--r--doc/api/metadata.md4
-rw-r--r--doc/development/i18n/proofreader.md1
-rw-r--r--doc/update/removals.md16
-rw-r--r--doc/user/group/saml_sso/index.md2
-rw-r--r--lib/api/metadata.rb2
-rw-r--r--lib/gitlab/background_migration/backfill_imported_issue_search_data.rb62
-rw-r--r--lib/gitlab/background_migration/batching_strategies/backfill_issue_work_item_type_batching_strategy.rb2
-rw-r--r--lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy.rb2
-rw-r--r--lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb23
-rw-r--r--lib/gitlab/ci/pipeline/chain/create.rb12
-rw-r--r--lib/gitlab/ci/tags/bulk_insert.rb36
-rw-r--r--lib/gitlab/database/background_migration/batched_job.rb3
-rw-r--r--lib/gitlab/database/background_migration/batched_migration_runner.rb3
-rw-r--r--locale/gitlab.pot27
-rw-r--r--qa/qa/page/merge_request/show.rb14
-rw-r--r--spec/controllers/graphql_controller_spec.rb12
-rw-r--r--spec/events/pages/page_deployed_event_spec.rb34
-rw-r--r--spec/features/admin/admin_sees_background_migrations_spec.rb4
-rw-r--r--spec/features/profiles/oauth_applications_spec.rb53
-rw-r--r--spec/frontend/issues/list/mock_data.js1
-rw-r--r--spec/frontend/issues/list/utils_spec.js8
-rw-r--r--spec/frontend/repository/commits_service_spec.js7
-rw-r--r--spec/frontend/repository/components/table/index_spec.js12
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js32
-rw-r--r--spec/frontend/vue_mr_widget/test_extensions.js33
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js56
-rw-r--r--spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb104
-rw-r--r--spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/create_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/tags/bulk_insert_spec.rb37
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_spec.rb4
-rw-r--r--spec/migrations/20220628012902_finalise_project_namespace_members_spec.rb72
-rw-r--r--spec/migrations/backfill_imported_issue_search_data_spec.rb56
-rw-r--r--spec/models/ci/build_spec.rb21
-rw-r--r--spec/models/ci/runner_spec.rb34
-rw-r--r--spec/models/commit_status_spec.rb16
-rw-r--r--spec/models/concerns/ci/bulk_insertable_tags_spec.rb66
-rw-r--r--spec/models/merge_request_spec.rb2
-rw-r--r--spec/models/namespace_spec.rb13
-rw-r--r--spec/models/project_spec.rb25
-rw-r--r--spec/presenters/blob_presenter_spec.rb2
-rw-r--r--spec/requests/admin/background_migrations_controller_spec.rb4
-rw-r--r--spec/services/ci/runners/register_runner_service_spec.rb41
-rw-r--r--spec/services/projects/update_pages_service_spec.rb10
-rw-r--r--spec/support/matchers/background_migrations_matchers.rb7
-rw-r--r--workhorse/internal/upstream/upstream.go7
80 files changed, 1212 insertions, 202 deletions
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 74f801f685c..a921eb62e26 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -90,6 +90,8 @@ export const UPDATED_ASC = 'UPDATED_ASC';
export const UPDATED_DESC = 'UPDATED_DESC';
export const WEIGHT_ASC = 'WEIGHT_ASC';
export const WEIGHT_DESC = 'WEIGHT_DESC';
+export const CLOSED_ASC = 'CLOSED_AT_ASC';
+export const CLOSED_DESC = 'CLOSED_AT_DESC';
export const urlSortParams = {
[PRIORITY_ASC]: 'priority',
@@ -98,6 +100,8 @@ export const urlSortParams = {
[CREATED_DESC]: 'created_date',
[UPDATED_ASC]: 'updated_asc',
[UPDATED_DESC]: 'updated_desc',
+ [CLOSED_ASC]: 'closed_asc',
+ [CLOSED_DESC]: 'closed_desc',
[MILESTONE_DUE_ASC]: 'milestone',
[MILESTONE_DUE_DESC]: 'milestone_due_desc',
[DUE_DATE_ASC]: 'due_date',
diff --git a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
index 73a13cea94a..35762120f71 100644
--- a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
@@ -13,6 +13,7 @@ fragment IssueFragment on Issue {
state
title
updatedAt
+ closedAt
upvotes
userDiscussionsCount @include(if: $isSignedIn)
webPath
diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js
index dfdc6e27f0d..6ba88e062af 100644
--- a/app/assets/javascripts/issues/list/utils.js
+++ b/app/assets/javascripts/issues/list/utils.js
@@ -44,6 +44,8 @@ import {
urlSortParams,
WEIGHT_ASC,
WEIGHT_DESC,
+ CLOSED_ASC,
+ CLOSED_DESC,
} from './constants';
export const getInitialPageParams = (
@@ -92,6 +94,14 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
},
{
id: 4,
+ title: __('Closed date'),
+ sortDirection: {
+ ascending: CLOSED_ASC,
+ descending: CLOSED_DESC,
+ },
+ },
+ {
+ id: 5,
title: __('Milestone due date'),
sortDirection: {
ascending: MILESTONE_DUE_ASC,
@@ -99,7 +109,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
},
},
{
- id: 5,
+ id: 6,
title: __('Due date'),
sortDirection: {
ascending: DUE_DATE_ASC,
@@ -107,7 +117,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
},
},
{
- id: 6,
+ id: 7,
title: __('Popularity'),
sortDirection: {
ascending: POPULARITY_ASC,
@@ -115,7 +125,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
},
},
{
- id: 7,
+ id: 8,
title: __('Label priority'),
sortDirection: {
ascending: LABEL_PRIORITY_ASC,
@@ -123,7 +133,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
},
},
{
- id: 8,
+ id: 9,
title: __('Manual'),
sortDirection: {
ascending: RELATIVE_POSITION_ASC,
@@ -131,7 +141,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
},
},
{
- id: 9,
+ id: 10,
title: __('Title'),
sortDirection: {
ascending: TITLE_ASC,
diff --git a/app/assets/javascripts/projects/commit_box/info/init_details_button.js b/app/assets/javascripts/projects/commit_box/info/init_details_button.js
index 833e946af5c..bc2c16b9e83 100644
--- a/app/assets/javascripts/projects/commit_box/info/init_details_button.js
+++ b/app/assets/javascripts/projects/commit_box/info/init_details_button.js
@@ -1,9 +1,7 @@
-import $ from 'jquery';
-
export const initDetailsButton = () => {
- $('body').on('click', '.js-details-expand', function expand(e) {
+ document.querySelector('.commit-info').addEventListener('click', function expand(e) {
e.preventDefault();
- $(this).next('.js-details-content').removeClass('hide');
- $(this).hide();
+ this.querySelector('.js-details-content').classList.remove('hide');
+ this.querySelector('.js-details-expand').classList.add('gl-display-none');
});
};
diff --git a/app/assets/javascripts/repository/commits_service.js b/app/assets/javascripts/repository/commits_service.js
index 5fd9cfd4e53..7f177da3ddd 100644
--- a/app/assets/javascripts/repository/commits_service.js
+++ b/app/assets/javascripts/repository/commits_service.js
@@ -35,7 +35,7 @@ const fetchData = (projectPath, path, ref, offset) => {
gon.relative_url_root || '/',
projectPath,
'/-/refs/',
- ref,
+ encodeURIComponent(ref),
'/logs_tree/',
encodeURIComponent(removeLeadingSlash(path)),
);
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index 41f7a4b147f..8faac62d4bb 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -2,6 +2,7 @@
import { GlSkeletonLoader, GlButton } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { sprintf, __ } from '~/locale';
+import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import getRefMixin from '../../mixins/get_ref';
import projectPathQuery from '../../queries/project_path.query.graphql';
import TableHeader from './header.vue';
@@ -103,13 +104,14 @@ export default {
return this.rowNumbers[key];
},
- getCommit(fileName, type) {
+ getCommit(flatPath, type) {
if (!this.glFeatures.lazyLoadCommits) {
return {};
}
return this.commits.find(
- (commitEntry) => commitEntry.fileName === fileName && commitEntry.type === type,
+ (commitEntry) =>
+ cleanLeadingSeparator(commitEntry.filePath) === flatPath && commitEntry.type === type,
);
},
},
@@ -152,7 +154,7 @@ export default {
:loading-path="loadingPath"
:total-entries="totalEntries"
:row-number="generateRowNumber(entry.flatPath, entry.id, index)"
- :commit-info="getCommit(entry.name, entry.type)"
+ :commit-info="getCommit(entry.flatPath, entry.type)"
v-on="$listeners"
/>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index 4ba620da00a..3ed961bcc1f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -194,6 +194,24 @@ export default {
poll.makeRequest();
},
+ initExtensionFullDataPolling() {
+ const poll = new Poll({
+ resource: {
+ fetchData: () => this.fetchFullData(this),
+ },
+ method: 'fetchData',
+ successCallback: (response) => {
+ this.headerCheck(response, (data) => {
+ this.setFullData(data);
+ });
+ },
+ errorCallback: (e) => {
+ this.setExpandedError(e);
+ },
+ });
+
+ poll.makeRequest();
+ },
headerCheck(response, callback) {
const headers = normalizeHeaders(response.headers);
@@ -220,6 +238,10 @@ export default {
});
}
},
+ setFullData(data) {
+ this.loadingState = null;
+ this.fullData = data.map((x, i) => ({ id: i, ...x }));
+ },
setCollapsedData(data) {
this.collapsedData = data;
this.loadingState = null;
@@ -229,21 +251,26 @@ export default {
Sentry.captureException(e);
},
+ setExpandedError(e) {
+ this.loadingState = LOADING_STATES.expandedError;
+ Sentry.captureException(e);
+ },
loadAllData() {
if (this.hasFullData) return;
this.loadingState = LOADING_STATES.expandedLoading;
- this.fetchFullData(this)
- .then((data) => {
- this.loadingState = null;
- this.fullData = data.map((x, i) => ({ id: i, ...x }));
- })
- .catch((e) => {
- this.loadingState = LOADING_STATES.expandedError;
-
- Sentry.captureException(e);
- });
+ if (this.$options.enableExpandedPolling) {
+ this.initExtensionFullDataPolling();
+ } else {
+ this.fetchFullData(this)
+ .then((data) => {
+ this.setFullData(data);
+ })
+ .catch((e) => {
+ this.setExpandedError(e);
+ });
+ }
},
appear(index) {
if (index === this.fullData.length - 1) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
index f4fcf4c9571..6adb12f9568 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
@@ -20,6 +20,7 @@ export const registerExtension = (extension) => {
i18n: extension.i18n,
expandEvent: extension.expandEvent,
enablePolling: extension.enablePolling,
+ enableExpandedPolling: extension.enableExpandedPolling,
modalComponent: extension.modalComponent,
computed: {
...extension.props.reduce(
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
index a9f8caa3e1f..468845da62d 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
@@ -86,7 +86,18 @@ export default {
createdAt() {
return getTimeago().format(this.issuable.createdAt);
},
- updatedAt() {
+ timestamp() {
+ if (this.issuable.state === 'closed') {
+ return this.issuable.closedAt;
+ }
+ return this.issuable.updatedAt;
+ },
+ formattedTimestamp() {
+ if (this.issuable.state === 'closed') {
+ return sprintf(__('closed %{timeago}'), {
+ timeago: getTimeago().format(this.issuable.closedAt),
+ });
+ }
return sprintf(__('updated %{timeAgo}'), {
timeAgo: getTimeago().format(this.issuable.updatedAt),
});
@@ -311,10 +322,10 @@ export default {
<div
v-gl-tooltip.bottom
class="gl-text-gray-500 gl-display-none gl-sm-display-inline-block"
- :title="tooltipTitle(issuable.updatedAt)"
- data-testid="issuable-updated-at"
+ :title="tooltipTitle(timestamp)"
+ data-testid="issuable-timestamp"
>
- {{ updatedAt }}
+ {{ formattedTimestamp }}
</div>
</div>
</li>
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index f3182af3047..e3e24ec0b53 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -709,10 +709,6 @@
line-height: 20px;
padding: 0;
}
-
- .issue-updated-at {
- line-height: 20px;
- }
}
@include media-breakpoint-down(xs) {
@@ -736,7 +732,7 @@
.issuable-milestone,
.issuable-info,
.task-status,
- .issuable-updated-at {
+ .issuable-timestamp {
font-weight: $gl-font-weight-normal;
color: $gl-text-color-secondary;
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index c71c101b434..67eeb43d5a2 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -82,6 +82,13 @@ class GraphqlController < ApplicationController
render_error(exception.message, status: :unprocessable_entity)
end
+ rescue_from ActiveRecord::QueryAborted do |exception|
+ log_exception(exception)
+
+ error = "Request timed out. Please try a less complex query or a smaller set of records."
+ render_error(error, status: :service_unavailable)
+ end
+
override :feature_category
def feature_category
::Gitlab::FeatureCategories.default.from_request(request) || super
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index 3724bb0d925..f425e996c2b 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -52,10 +52,8 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
end
def set_index_vars
- @applications = current_user.oauth_applications
- @authorized_tokens = current_user.oauth_authorized_tokens
- @authorized_anonymous_tokens = @authorized_tokens.reject(&:application)
- @authorized_apps = @authorized_tokens.map(&:application).uniq.reject(&:nil?)
+ @applications = current_user.oauth_applications.load
+ @authorized_tokens = current_user.oauth_authorized_tokens.preload(:application).order(created_at: :desc).load # rubocop: disable CodeReuse/ActiveRecord
# Don't overwrite a value possibly set by `create`
@application ||= Doorkeeper::Application.new
diff --git a/app/events/pages/page_deployed_event.rb b/app/events/pages/page_deployed_event.rb
new file mode 100644
index 00000000000..52e53772a51
--- /dev/null
+++ b/app/events/pages/page_deployed_event.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Pages
+ class PageDeployedEvent < ::Gitlab::EventStore::Event
+ def schema
+ {
+ 'type' => 'object',
+ 'properties' => {
+ 'project_id' => { 'type' => 'integer' },
+ 'namespace_id' => { 'type' => 'integer' },
+ 'root_namespace_id' => { 'type' => 'integer' }
+ },
+ 'required' => %w[project_id namespace_id root_namespace_id]
+ }
+ end
+ end
+end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 6f15cc7f4ec..ef79e2bc86f 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -254,6 +254,7 @@ module SortingHelper
options = [
{ value: sort_value_priority, text: sort_title_priority, href: page_filter_path(sort: sort_value_priority) },
{ value: sort_value_created_date, text: sort_title_created_date, href: page_filter_path(sort: sort_value_created_date) },
+ { value: sort_value_closed_date, text: sort_title_closed_date, href: page_filter_path(sort: sort_value_closed_date) },
{ value: sort_value_recently_updated, text: sort_title_recently_updated, href: page_filter_path(sort: sort_value_recently_updated) },
{ value: sort_value_milestone, text: sort_title_milestone, href: page_filter_path(sort: sort_value_milestone) }
]
@@ -261,7 +262,7 @@ module SortingHelper
options.concat([due_date_option]) if viewing_issues
options.concat([popularity_option, label_priority_option])
- options.concat([merged_option, closed_option]) if viewing_merge_requests
+ options.concat([merged_option]) if viewing_merge_requests
options.concat([relative_position_option]) if viewing_issues
options.concat([title_option])
@@ -287,10 +288,6 @@ module SortingHelper
{ value: sort_value_merged_date, text: sort_title_merged_date, href: page_filter_path(sort: sort_value_merged_date) }
end
- def closed_option
- { value: sort_value_closed_date, text: sort_title_closed_date, href: page_filter_path(sort: sort_value_closed_date) }
- end
-
def relative_position_option
{ value: sort_value_relative_position, text: sort_title_relative_position, href: page_filter_path(sort: sort_value_relative_position) }
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 24f3161bd35..cdeccedc147 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -2,6 +2,7 @@
module Ci
class Build < Ci::Processable
+ prepend Ci::BulkInsertableTags
include Ci::Metadatable
include Ci::Contextable
include TokenAuthenticatable
@@ -434,10 +435,6 @@ module Ci
true
end
- def save_tags
- super unless Thread.current['ci_bulk_insert_tags']
- end
-
def archived?
return true if degenerated?
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 61194c9b7d1..e0180880760 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -2,6 +2,7 @@
module Ci
class Runner < Ci::ApplicationRecord
+ prepend Ci::BulkInsertableTags
include Gitlab::SQL::Pattern
include RedisCacheable
include ChronicDurationAttribute
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index ac9d8c39bd2..d08d303912d 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -220,10 +220,6 @@ class CommitStatus < Ci::ApplicationRecord
false
end
- def self.bulk_insert_tags!(statuses)
- Gitlab::Ci::Tags::BulkInsert.new(statuses).insert!
- end
-
def locking_enabled?
will_save_change_to_status?
end
diff --git a/app/models/concerns/ci/bulk_insertable_tags.rb b/app/models/concerns/ci/bulk_insertable_tags.rb
new file mode 100644
index 00000000000..453b3b3fbc9
--- /dev/null
+++ b/app/models/concerns/ci/bulk_insertable_tags.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Ci
+ module BulkInsertableTags
+ extend ActiveSupport::Concern
+
+ BULK_INSERT_TAG_THREAD_KEY = 'ci_bulk_insert_tags'
+
+ class << self
+ def with_bulk_insert_tags
+ previous = Thread.current[BULK_INSERT_TAG_THREAD_KEY]
+ Thread.current[BULK_INSERT_TAG_THREAD_KEY] = true
+ yield
+ ensure
+ Thread.current[BULK_INSERT_TAG_THREAD_KEY] = previous
+ end
+ end
+
+ # overrides save_tags from acts-as-taggable
+ def save_tags
+ super unless Thread.current[BULK_INSERT_TAG_THREAD_KEY]
+ end
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 47aa2b24feb..daad3c7c691 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -332,7 +332,7 @@ class Issue < ApplicationRecord
when 'severity_desc' then order_severity_desc.with_order_id_desc
when 'escalation_status_asc' then order_escalation_status_asc.with_order_id_desc
when 'escalation_status_desc' then order_escalation_status_desc.with_order_id_desc
- when 'closed_at_asc' then order_closed_at_asc
+ when 'closed_at', 'closed_at_asc' then order_closed_at_asc
when 'closed_at_desc' then order_closed_at_desc
else
super
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index 2dcc6cd5df3..74ac47fa439 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -69,7 +69,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
end
def find_file_path
- url_helpers.project_find_file_path(project, ref_qualified_path)
+ url_helpers.project_find_file_path(project, blob.commit_id)
end
def blame_path
diff --git a/app/services/ci/runners/register_runner_service.rb b/app/services/ci/runners/register_runner_service.rb
index 196d2de1a65..6588cd7e248 100644
--- a/app/services/ci/runners/register_runner_service.rb
+++ b/app/services/ci/runners/register_runner_service.rb
@@ -8,7 +8,19 @@ module Ci
return unless runner_type_attrs
- ::Ci::Runner.create(attributes.merge(runner_type_attrs))
+ runner = ::Ci::Runner.new(attributes.merge(runner_type_attrs))
+
+ Ci::BulkInsertableTags.with_bulk_insert_tags do
+ Ci::Runner.transaction do
+ if runner.save
+ Gitlab::Ci::Tags::BulkInsert.bulk_insert_tags!([runner])
+ else
+ raise ActiveRecord::Rollback
+ end
+ end
+ end
+
+ runner
end
private
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 8ded2516b97..217c492bd72 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -53,6 +53,7 @@ module Projects
def success
@commit_status.success
@project.mark_pages_as_deployed
+ publish_deployed_event
super
end
@@ -203,6 +204,16 @@ module Projects
def pages_file_entries_limit
project.actual_limits.pages_file_entries
end
+
+ def publish_deployed_event
+ event = ::Pages::PageDeployedEvent.new(data: {
+ project_id: project.id,
+ namespace_id: project.namespace_id,
+ root_namespace_id: project.root_namespace.id
+ })
+
+ Gitlab::EventStore.publish(event)
+ end
end
end
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
index af5ad06d30e..2e024b8ffc4 100644
--- a/app/views/projects/find_file/show.html.haml
+++ b/app/views/projects/find_file/show.html.haml
@@ -1,6 +1,6 @@
- page_title _("Find File"), @ref
-.file-finder-holder.tree-holder.clearfix.js-file-finder{ 'data-file-find-url': "#{escape_javascript(project_files_path(@project, @ref, format: :json))}", 'data-find-tree-url': escape_javascript(project_tree_path(@project, @ref)), 'data-blob-url-template': escape_javascript(project_blob_path(@project, @id || @commit.id)) }
+.file-finder-holder.tree-holder.clearfix.js-file-finder{ 'data-file-find-url': "#{escape_javascript(project_files_path(@project, @ref, format: :json))}", 'data-find-tree-url': escape_javascript(project_tree_path(@project, @ref)), 'data-blob-url-template': escape_javascript(project_blob_path(@project, @ref)) }
.nav-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'find_file', path: @path
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 4c96875ce42..2e70cb28267 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -65,6 +65,9 @@
= render 'shared/issuable_meta_data', issuable: issue
- .float-right.issuable-updated-at.d-none.d-sm-inline-block
+ .float-right.issuable-timestamp.d-none.d-sm-inline-block
%span
- = _('updated %{time_ago}').html_safe % { time_ago: time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago') }
+ - if issue.closed?
+ = _('closed %{timeago}').html_safe % { timeago: time_ago_with_tooltip(issue.closed_at, placement: 'bottom') }
+ - else
+ = _('updated %{time_ago}').html_safe % { time_ago: time_ago_with_tooltip(issue.updated_at, placement: 'bottom') }
diff --git a/app/views/shared/doorkeeper/applications/_index.html.haml b/app/views/shared/doorkeeper/applications/_index.html.haml
index 0359c28794c..b14ff9b2508 100644
--- a/app/views/shared/doorkeeper/applications/_index.html.haml
+++ b/app/views/shared/doorkeeper/applications/_index.html.haml
@@ -55,7 +55,7 @@
.oauth-authorized-applications.prepend-top-20.gl-mb-3
- if oauth_applications_enabled
%h5
- = _("Authorized applications (%{size})") % { size: @authorized_apps.size + @authorized_anonymous_tokens.size }
+ = _("Authorized applications (%{size})") % { size: @authorized_tokens.size }
- if @authorized_tokens.any?
.table-responsive
@@ -67,22 +67,22 @@
%th= _('Scope')
%th
%tbody
- - @authorized_apps.each do |app|
- - token = app.authorized_tokens.order('created_at desc').first # rubocop: disable CodeReuse/ActiveRecord
- %tr{ id: "application_#{app.id}" }
- %td= app.name
- %td= token.created_at
- %td= token.scopes
- %td= render 'doorkeeper/authorized_applications/delete_form', application: app
- - @authorized_anonymous_tokens.each do |token|
- %tr
+ - @authorized_tokens.each do |token|
+ %tr{ id: ("application_#{token.application.id}" if token.application) }
%td
- = _('Anonymous')
- .form-text.text-muted
- %em= _("Authorization was granted by entering your username and password in the application.")
+ - if token.application
+ = token.application.name
+ - else
+ = _('Anonymous')
+ .form-text.text-muted
+ %em= _("Authorization was granted by entering your username and password in the application.")
%td= token.created_at
%td= token.scopes
- %td= render 'doorkeeper/authorized_applications/delete_form', token: token
+ %td
+ - if token.application
+ = render 'doorkeeper/authorized_applications/delete_form', application: token.application
+ - else
+ = render 'doorkeeper/authorized_applications/delete_form', token: token
- else
.settings-message.text-center
= _("You don't have any authorized applications")
diff --git a/data/removals/15_0/15-0-configure-self-managed-cert-based-kube-feature-flag.yml b/data/removals/15_0/15-0-configure-self-managed-cert-based-kube-feature-flag.yml
new file mode 100644
index 00000000000..a4b8b422dd9
--- /dev/null
+++ b/data/removals/15_0/15-0-configure-self-managed-cert-based-kube-feature-flag.yml
@@ -0,0 +1,24 @@
+- name: "Self-managed certificate-based integration with Kubernetes feature flagged"
+ announcement_milestone: "14.5"
+ announcement_date: "2021-11-15"
+ removal_milestone: "15.0"
+ removal_date: "2022-05-22"
+ breaking_change: true
+ reporter: nagyv-gitlab
+ stage: Configure
+ issue_url: https://gitlab.com/groups/gitlab-org/configure/-/epics/8
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ In 15.0 the certificate-based integration with Kubernetes will be disabled by default.
+
+ After 15.0, you should use the [agent for Kubernetes](https://docs.gitlab.com/ee/user/clusters/agent/) to connect Kubernetes clusters with GitLab. The agent for Kubernetes is a more robust, secure, and reliable integration with Kubernetes. [How do I migrate to the agent?](https://docs.gitlab.com/ee/user/infrastructure/clusters/migrate_to_gitlab_agent.html)
+
+ If you need more time to migrate, you can enable the `certificate_based_clusters` [feature flag](https://docs.gitlab.com/ee/administration/feature_flags.html), which re-enables the certificate-based integration.
+
+ In GitLab 16.0, we will [remove the feature, its related code, and the feature flag](https://about.gitlab.com/blog/2021/11/15/deprecating-the-cert-based-kubernetes-integration/). GitLab will continue to fix any security or critical issues until 16.0.
+
+ For updates and details, follow [this epic](https://gitlab.com/groups/gitlab-org/configure/-/epics/8).
+#
+# OPTIONAL FIELDS
+#
+ tiers: [Core, Premium, Ultimate]
+ documentation_url: 'https://docs.gitlab.com/ee/user/infrastructure/clusters/#certificate-based-kubernetes-integration-deprecated'
diff --git a/db/migrate/20220621202616_add_partial_index_on_oauth_access_tokens_revoked_at.rb b/db/migrate/20220621202616_add_partial_index_on_oauth_access_tokens_revoked_at.rb
new file mode 100644
index 00000000000..2222698dcea
--- /dev/null
+++ b/db/migrate/20220621202616_add_partial_index_on_oauth_access_tokens_revoked_at.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddPartialIndexOnOauthAccessTokensRevokedAt < Gitlab::Database::Migration[2.0]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'partial_index_resource_owner_id_created_at_token_not_revoked'
+ EXISTING_INDEX_NAME = 'index_oauth_access_tokens_on_resource_owner_id'
+
+ def up
+ add_concurrent_index :oauth_access_tokens, [:resource_owner_id, :created_at],
+ name: INDEX_NAME, where: 'revoked_at IS NULL'
+ remove_concurrent_index :oauth_access_tokens, :resource_owner_id, name: EXISTING_INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index :oauth_access_tokens, :resource_owner_id, name: EXISTING_INDEX_NAME
+ remove_concurrent_index :oauth_access_tokens, [:resource_owner_id, :created_at], name: INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20220621040800_backfill_imported_issue_search_data.rb b/db/post_migrate/20220621040800_backfill_imported_issue_search_data.rb
new file mode 100644
index 00000000000..39df2b43168
--- /dev/null
+++ b/db/post_migrate/20220621040800_backfill_imported_issue_search_data.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class BackfillImportedIssueSearchData < Gitlab::Database::Migration[2.0]
+ disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ MIGRATION = 'BackfillImportedIssueSearchData'
+ DELAY_INTERVAL = 120.seconds
+
+ def up
+ min_value = Gitlab::Database::BackgroundMigration::BatchedMigration.find_by(
+ job_class_name: "BackfillIssueSearchData"
+ )&.max_value || BATCH_MIN_VALUE
+ queue_batched_background_migration(
+ MIGRATION,
+ :issues,
+ :id,
+ job_interval: DELAY_INTERVAL,
+ batch_min_value: min_value
+ )
+ end
+
+ def down
+ delete_batched_background_migration(MIGRATION, :issues, :id, [])
+ end
+end
diff --git a/db/post_migrate/20220628012902_finalise_project_namespace_members.rb b/db/post_migrate/20220628012902_finalise_project_namespace_members.rb
new file mode 100644
index 00000000000..29b11fb4357
--- /dev/null
+++ b/db/post_migrate/20220628012902_finalise_project_namespace_members.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class FinaliseProjectNamespaceMembers < Gitlab::Database::Migration[2.0]
+ MIGRATION = 'BackfillProjectMemberNamespaceId'
+ disable_ddl_transaction!
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ ensure_batched_background_migration_is_finished(
+ job_class_name: MIGRATION,
+ table_name: :members,
+ column_name: :id,
+ job_arguments: [],
+ finalize: true
+ )
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema_migrations/20220621040800 b/db/schema_migrations/20220621040800
new file mode 100644
index 00000000000..dbdc38367be
--- /dev/null
+++ b/db/schema_migrations/20220621040800
@@ -0,0 +1 @@
+effd82de862e39edcba7793010bdd377b8141c49edebdd380276a8b558886835 \ No newline at end of file
diff --git a/db/schema_migrations/20220621202616 b/db/schema_migrations/20220621202616
new file mode 100644
index 00000000000..187ff41b3c1
--- /dev/null
+++ b/db/schema_migrations/20220621202616
@@ -0,0 +1 @@
+6567c86c14f741b7ea8f49b04c3ad82f226f04c0ab2e68212b5f6e7bf4ef615f \ No newline at end of file
diff --git a/db/schema_migrations/20220628012902 b/db/schema_migrations/20220628012902
new file mode 100644
index 00000000000..ef7325629ca
--- /dev/null
+++ b/db/schema_migrations/20220628012902
@@ -0,0 +1 @@
+5881441f8a6c0f25cff00aa9e164a1c19bcc34d4db678fc50712824fff82b24e \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 7a966564e06..34bfb8380f6 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -28740,8 +28740,6 @@ CREATE INDEX index_oauth_access_tokens_on_application_id ON oauth_access_tokens
CREATE UNIQUE INDEX index_oauth_access_tokens_on_refresh_token ON oauth_access_tokens USING btree (refresh_token);
-CREATE INDEX index_oauth_access_tokens_on_resource_owner_id ON oauth_access_tokens USING btree (resource_owner_id);
-
CREATE UNIQUE INDEX index_oauth_access_tokens_on_token ON oauth_access_tokens USING btree (token);
CREATE INDEX index_oauth_applications_on_owner_id_and_owner_type ON oauth_applications USING btree (owner_id, owner_type);
@@ -30022,6 +30020,8 @@ CREATE INDEX partial_index_deployments_for_legacy_successful_deployments ON depl
CREATE INDEX partial_index_deployments_for_project_id_and_tag ON deployments USING btree (project_id) WHERE (tag IS TRUE);
+CREATE INDEX partial_index_resource_owner_id_created_at_token_not_revoked ON oauth_access_tokens USING btree (resource_owner_id, created_at) WHERE (revoked_at IS NULL);
+
CREATE INDEX partial_index_slack_integrations_with_bot_user_id ON slack_integrations USING btree (id) WHERE (bot_user_id IS NOT NULL);
CREATE UNIQUE INDEX partial_index_sop_configs_on_namespace_id ON security_orchestration_policy_configurations USING btree (namespace_id) WHERE (namespace_id IS NOT NULL);
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index c1cf670e744..af91c842838 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -7185,6 +7185,29 @@ The edge type for [`IncidentManagementOncallShift`](#incidentmanagementoncallshi
| <a id="incidentmanagementoncallshiftedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="incidentmanagementoncallshiftedgenode"></a>`node` | [`IncidentManagementOncallShift`](#incidentmanagementoncallshift) | The item at the end of the edge. |
+#### `IssuableResourceLinkConnection`
+
+The connection type for [`IssuableResourceLink`](#issuableresourcelink).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="issuableresourcelinkconnectionedges"></a>`edges` | [`[IssuableResourceLinkEdge]`](#issuableresourcelinkedge) | A list of edges. |
+| <a id="issuableresourcelinkconnectionnodes"></a>`nodes` | [`[IssuableResourceLink]`](#issuableresourcelink) | A list of nodes. |
+| <a id="issuableresourcelinkconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `IssuableResourceLinkEdge`
+
+The edge type for [`IssuableResourceLink`](#issuableresourcelink).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="issuableresourcelinkedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="issuableresourcelinkedgenode"></a>`node` | [`IssuableResourceLink`](#issuableresourcelink) | The item at the end of the edge. |
+
#### `IssueConnection`
The connection type for [`Issue`](#issue).
@@ -11342,6 +11365,22 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- |
| <a id="epicissuecurrentusertodosstate"></a>`state` | [`TodoStateEnum`](#todostateenum) | State of the to-do items. |
+##### `EpicIssue.issuableResourceLinks`
+
+Issuable resource links of the incident issue.
+
+Returns [`IssuableResourceLinkConnection`](#issuableresourcelinkconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#connection-pagination-arguments):
+`before: String`, `after: String`, `first: Int`, `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="epicissueissuableresourcelinksincidentid"></a>`incidentId` | [`IssueID!`](#issueid) | ID of the incident. |
+
##### `EpicIssue.reference`
Internal reference of the issue. Returned in shortened format by default.
@@ -12717,6 +12756,22 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- |
| <a id="issuecurrentusertodosstate"></a>`state` | [`TodoStateEnum`](#todostateenum) | State of the to-do items. |
+##### `Issue.issuableResourceLinks`
+
+Issuable resource links of the incident issue.
+
+Returns [`IssuableResourceLinkConnection`](#issuableresourcelinkconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#connection-pagination-arguments):
+`before: String`, `after: String`, `first: Int`, `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="issueissuableresourcelinksincidentid"></a>`incidentId` | [`IssueID!`](#issueid) | ID of the incident. |
+
##### `Issue.reference`
Internal reference of the issue. Returned in shortened format by default.
diff --git a/doc/api/metadata.md b/doc/api/metadata.md
index fc6571dd1be..deb2654f22b 100644
--- a/doc/api/metadata.md
+++ b/doc/api/metadata.md
@@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Metadata API **(FREE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/357032) in GitLab 15.1.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/357032) in GitLab 15.2.
Retrieve metadata information for this GitLab instance.
@@ -35,7 +35,7 @@ Example response:
```json
{
- "version": "15.0-pre",
+ "version": "15.2-pre",
"revision": "c401a659d0c",
"kas": {
"enabled": true,
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index 8231cf4316b..9431fce4255 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -54,6 +54,7 @@ are very appreciative of the work done by translators and proofreaders!
- Andrei Jiroh Halili - [GitLab](https://gitlab.com/ajhalili2006), [Crowdin](https://crowdin.com/profile/AndreiJirohHaliliDev2006)
- French
- Davy Defaud - [GitLab](https://gitlab.com/DevDef), [Crowdin](https://crowdin.com/profile/DevDef)
+ - Germain Gorisse - [GitLab](https://gitlab.com/ggorisse), [Crowdin](https://crowdin.com/profile/germaingorisse)
- Galician
- Antón Méixome - [Crowdin](https://crowdin.com/profile/meixome)
- Pedro Garcia - [GitLab](https://gitlab.com/pedgarrod), [Crowdin](https://crowdin.com/profile/breaking_pitt)
diff --git a/doc/update/removals.md b/doc/update/removals.md
index d183154234b..32fbf851176 100644
--- a/doc/update/removals.md
+++ b/doc/update/removals.md
@@ -447,6 +447,22 @@ You can still customize the behavior of the Secret Detection analyzer using the
For further details, see [the deprecation issue for this change](https://gitlab.com/gitlab-org/gitlab/-/issues/352565).
+### Self-managed certificate-based integration with Kubernetes feature flagged
+
+WARNING:
+This is a [breaking change](https://docs.gitlab.com/ee/development/contributing/#breaking-changes).
+Review the details carefully before upgrading.
+
+In 15.0 the certificate-based integration with Kubernetes will be disabled by default.
+
+After 15.0, you should use the [agent for Kubernetes](https://docs.gitlab.com/ee/user/clusters/agent/) to connect Kubernetes clusters with GitLab. The agent for Kubernetes is a more robust, secure, and reliable integration with Kubernetes. [How do I migrate to the agent?](https://docs.gitlab.com/ee/user/infrastructure/clusters/migrate_to_gitlab_agent.html)
+
+If you need more time to migrate, you can enable the `certificate_based_clusters` [feature flag](https://docs.gitlab.com/ee/administration/feature_flags.html), which re-enables the certificate-based integration.
+
+In GitLab 16.0, we will [remove the feature, its related code, and the feature flag](https://about.gitlab.com/blog/2021/11/15/deprecating-the-cert-based-kubernetes-integration/). GitLab will continue to fix any security or critical issues until 16.0.
+
+For updates and details, follow [this epic](https://gitlab.com/groups/gitlab-org/configure/-/epics/8).
+
### Sidekiq configuration for metrics and health checks
WARNING:
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index 5a568603623..d056839c167 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -513,7 +513,7 @@ Alternatively, the SAML response may be missing the `InResponseTo` attribute in
The identity provider administrator should ensure that the login is
initiated by the service provider and not the identity provider.
-### Message: "Login to a GitLab account to link with your SAML identity"
+### Message: "Sign in to GitLab to connect your organization's account"
A user can see this message when they are trying to [manually link SAML to their existing GitLab.com account](#linking-saml-to-your-existing-gitlabcom-account).
diff --git a/lib/api/metadata.rb b/lib/api/metadata.rb
index c2e4b52bbef..c4984f0e7f0 100644
--- a/lib/api/metadata.rb
+++ b/lib/api/metadata.rb
@@ -26,7 +26,7 @@ module API
EOF
desc 'Get the metadata information of the GitLab instance.' do
- detail 'This feature was introduced in GitLab 15.1.'
+ detail 'This feature was introduced in GitLab 15.2.'
end
get '/metadata' do
run_graphql!(
diff --git a/lib/gitlab/background_migration/backfill_imported_issue_search_data.rb b/lib/gitlab/background_migration/backfill_imported_issue_search_data.rb
new file mode 100644
index 00000000000..d2077a24d8c
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_imported_issue_search_data.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ # Backfills the `issue_search_data` table for issues imported prior
+ # to the fix for the imported issues search data bug:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/361219
+ class BackfillImportedIssueSearchData < BatchedMigrationJob
+ def perform
+ each_sub_batch(
+ operation_name: :update_search_data,
+ batching_scope: -> (relation) { Issue }
+ ) do |sub_batch|
+ update_search_data(sub_batch)
+ rescue ActiveRecord::StatementInvalid => e
+ raise unless e.cause.is_a?(PG::ProgramLimitExceeded) && e.message.include?('string is too long for tsvector')
+
+ update_search_data_individually(sub_batch)
+ end
+ end
+
+ private
+
+ def update_search_data(relation)
+ ApplicationRecord.connection.execute(
+ <<~SQL
+ INSERT INTO issue_search_data
+ SELECT
+ project_id,
+ id,
+ NOW(),
+ NOW(),
+ setweight(to_tsvector('english', LEFT(title, 255)), 'A') || setweight(to_tsvector('english', LEFT(REGEXP_REPLACE(description, '[A-Za-z0-9+/@]{50,}', ' ', 'g'), 1048576)), 'B')
+ FROM issues
+ WHERE issues.id IN (#{relation.select(:id).to_sql})
+ ON CONFLICT DO NOTHING
+ SQL
+ )
+ end
+
+ def update_search_data_individually(relation)
+ relation.pluck(:id).each do |issue_id|
+ update_search_data(relation.klass.where(id: issue_id))
+ sleep(pause_ms * 0.001)
+ rescue ActiveRecord::StatementInvalid => e
+ raise unless e.cause.is_a?(PG::ProgramLimitExceeded) && e.message.include?('string is too long for tsvector')
+
+ logger.error(
+ message: "Error updating search data: #{e.message}",
+ class: relation.klass.name,
+ model_id: issue_id
+ )
+ end
+ end
+
+ def logger
+ @logger ||= Gitlab::BackgroundMigration::Logger.build
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/batching_strategies/backfill_issue_work_item_type_batching_strategy.rb b/lib/gitlab/background_migration/batching_strategies/backfill_issue_work_item_type_batching_strategy.rb
index 06036eebcb9..7d5fef67c25 100644
--- a/lib/gitlab/background_migration/batching_strategies/backfill_issue_work_item_type_batching_strategy.rb
+++ b/lib/gitlab/background_migration/batching_strategies/backfill_issue_work_item_type_batching_strategy.rb
@@ -8,7 +8,7 @@ module Gitlab
#
# If no more batches exist in the table, returns nil.
class BackfillIssueWorkItemTypeBatchingStrategy < PrimaryKeyBatchingStrategy
- def apply_additional_filters(relation, job_arguments:)
+ def apply_additional_filters(relation, job_arguments:, job_class: nil)
issue_type = job_arguments.first
relation.where(issue_type: issue_type)
diff --git a/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy.rb b/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy.rb
index f352c527b54..68be42dc0a0 100644
--- a/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy.rb
+++ b/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy.rb
@@ -16,7 +16,7 @@ module Gitlab
# batch_min_value - The minimum value which the next batch will start at
# batch_size - The size of the next batch
# job_arguments - The migration job arguments
- def next_batch(table_name, column_name, batch_min_value:, batch_size:, job_arguments:)
+ def next_batch(table_name, column_name, batch_min_value:, batch_size:, job_arguments:, job_class: nil)
next_batch_bounds = nil
model_class = ::Gitlab::BackgroundMigration::ProjectNamespaces::Models::Project
quoted_column_name = model_class.connection.quote_column_name(column_name)
diff --git a/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb b/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb
index e7a68b183b8..c2f59bf9c76 100644
--- a/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb
+++ b/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb
@@ -18,12 +18,13 @@ module Gitlab
# batch_min_value - The minimum value which the next batch will start at
# batch_size - The size of the next batch
# job_arguments - The migration job arguments
- def next_batch(table_name, column_name, batch_min_value:, batch_size:, job_arguments:)
+ # job_class - The migration job class
+ def next_batch(table_name, column_name, batch_min_value:, batch_size:, job_arguments:, job_class: nil)
model_class = define_batchable_model(table_name, connection: connection)
quoted_column_name = model_class.connection.quote_column_name(column_name)
relation = model_class.where("#{quoted_column_name} >= ?", batch_min_value)
- relation = apply_additional_filters(relation, job_arguments: job_arguments)
+ relation = apply_additional_filters(relation, job_arguments: job_arguments, job_class: job_class)
next_batch_bounds = nil
relation.each_batch(of: batch_size, column: column_name) do |batch| # rubocop:disable Lint/UnreachableLoop
@@ -35,19 +36,11 @@ module Gitlab
next_batch_bounds
end
- # Strategies based on PrimaryKeyBatchingStrategy can use
- # this method to easily apply additional filters.
- #
- # Example:
- #
- # class MatchingType < PrimaryKeyBatchingStrategy
- # def apply_additional_filters(relation, job_arguments:)
- # type = job_arguments.first
- #
- # relation.where(type: type)
- # end
- # end
- def apply_additional_filters(relation, job_arguments: [])
+ def apply_additional_filters(relation, job_arguments: [], job_class: nil)
+ if job_class.respond_to?(:batching_scope)
+ return job_class.batching_scope(relation, job_arguments: job_arguments)
+ end
+
relation
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/create.rb b/lib/gitlab/ci/pipeline/chain/create.rb
index 71dfc1a676c..207b4b5ff8b 100644
--- a/lib/gitlab/ci/pipeline/chain/create.rb
+++ b/lib/gitlab/ci/pipeline/chain/create.rb
@@ -11,10 +11,10 @@ module Gitlab
def perform!
logger.instrument_with_sql(:pipeline_save) do
BulkInsertableAssociations.with_bulk_insert do
- with_bulk_insert_tags do
+ ::Ci::BulkInsertableTags.with_bulk_insert_tags do
pipeline.transaction do
pipeline.save!
- CommitStatus.bulk_insert_tags!(statuses)
+ Gitlab::Ci::Tags::BulkInsert.bulk_insert_tags!(statuses)
end
end
end
@@ -29,14 +29,6 @@ module Gitlab
private
- def with_bulk_insert_tags
- previous = Thread.current['ci_bulk_insert_tags']
- Thread.current['ci_bulk_insert_tags'] = true
- yield
- ensure
- Thread.current['ci_bulk_insert_tags'] = previous
- end
-
def statuses
strong_memoize(:statuses) do
pipeline
diff --git a/lib/gitlab/ci/tags/bulk_insert.rb b/lib/gitlab/ci/tags/bulk_insert.rb
index 870bd0fc0a2..2e56e47f5b8 100644
--- a/lib/gitlab/ci/tags/bulk_insert.rb
+++ b/lib/gitlab/ci/tags/bulk_insert.rb
@@ -9,33 +9,37 @@ module Gitlab
TAGGINGS_BATCH_SIZE = 1000
TAGS_BATCH_SIZE = 500
- def initialize(statuses)
- @statuses = statuses
+ def self.bulk_insert_tags!(taggables)
+ Gitlab::Ci::Tags::BulkInsert.new(taggables).insert!
+ end
+
+ def initialize(taggables)
+ @taggables = taggables
end
def insert!
- return false if tag_list_by_status.empty?
+ return false if tag_list_by_taggable.empty?
persist_build_tags!
end
private
- attr_reader :statuses
+ attr_reader :taggables
- def tag_list_by_status
- strong_memoize(:tag_list_by_status) do
- statuses.each.with_object({}) do |status, acc|
- tag_list = status.tag_list
+ def tag_list_by_taggable
+ strong_memoize(:tag_list_by_taggable) do
+ taggables.each.with_object({}) do |taggable, acc|
+ tag_list = taggable.tag_list
next unless tag_list
- acc[status] = tag_list
+ acc[taggable] = tag_list
end
end
end
def persist_build_tags!
- all_tags = tag_list_by_status.values.flatten.uniq.reject(&:blank?)
+ all_tags = tag_list_by_taggable.values.flatten.uniq.reject(&:blank?)
tag_records_by_name = create_tags(all_tags).index_by(&:name)
taggings = build_taggings_attributes(tag_records_by_name)
@@ -65,24 +69,24 @@ module Gitlab
# rubocop: enable CodeReuse/ActiveRecord
def build_taggings_attributes(tag_records_by_name)
- taggings = statuses.flat_map do |status|
- tag_list = tag_list_by_status[status]
+ taggings = taggables.flat_map do |taggable|
+ tag_list = tag_list_by_taggable[taggable]
next unless tag_list
tags = tag_records_by_name.values_at(*tag_list)
- taggings_for(tags, status)
+ taggings_for(tags, taggable)
end
taggings.compact!
taggings
end
- def taggings_for(tags, status)
+ def taggings_for(tags, taggable)
tags.map do |tag|
{
tag_id: tag.id,
- taggable_type: CommitStatus.name,
- taggable_id: status.id,
+ taggable_type: taggable.class.base_class.name,
+ taggable_id: taggable.id,
created_at: Time.current,
context: 'tags'
}
diff --git a/lib/gitlab/database/background_migration/batched_job.rb b/lib/gitlab/database/background_migration/batched_job.rb
index ebc3ee240bd..436403e39ab 100644
--- a/lib/gitlab/database/background_migration/batched_job.rb
+++ b/lib/gitlab/database/background_migration/batched_job.rb
@@ -128,7 +128,8 @@ module Gitlab
batched_migration.column_name,
batch_min_value: min_value,
batch_size: new_batch_size,
- job_arguments: batched_migration.job_arguments
+ job_arguments: batched_migration.job_arguments,
+ job_class: batched_migration.job_class
)
midpoint = next_batch_bounds.last
diff --git a/lib/gitlab/database/background_migration/batched_migration_runner.rb b/lib/gitlab/database/background_migration/batched_migration_runner.rb
index 388eb596ce2..d15886f02b8 100644
--- a/lib/gitlab/database/background_migration/batched_migration_runner.rb
+++ b/lib/gitlab/database/background_migration/batched_migration_runner.rb
@@ -101,7 +101,8 @@ module Gitlab
active_migration.column_name,
batch_min_value: batch_min_value,
batch_size: active_migration.batch_size,
- job_arguments: active_migration.job_arguments)
+ job_arguments: active_migration.job_arguments,
+ job_class: active_migration.job_class)
return if next_batch_bounds.nil?
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e7f0fb07fa8..b97c4e6ec28 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8220,6 +8220,9 @@ msgstr ""
msgid "Closed MRs"
msgstr ""
+msgid "Closed date"
+msgstr ""
+
msgid "Closed issues"
msgstr ""
@@ -33478,6 +33481,21 @@ msgstr ""
msgid "SAML for %{group_name}"
msgstr ""
+msgid "SAML|Selecting \"Authorize\" will transfer ownership of your GitLab account \"%{username}\" (%{email}) to your organization."
+msgstr ""
+
+msgid "SAML|Sign in to GitLab to connect your organization's account"
+msgstr ""
+
+msgid "SAML|The \"%{group_path}\" group allows you to sign in with your Single Sign-On Account."
+msgstr ""
+
+msgid "SAML|To access \"%{group_name}\" you must sign in with your Single Sign-On account, through an external sign-in page."
+msgstr ""
+
+msgid "SAML|Your organization's SSO has been connected to your GitLab account"
+msgstr ""
+
msgid "SAST Configuration"
msgstr ""
@@ -38177,9 +38195,6 @@ msgstr ""
msgid "That's it, well done!"
msgstr ""
-msgid "The \"%{group_path}\" group allows you to sign in with your Single Sign-On Account"
-msgstr ""
-
msgid "The %{link_start}true-up model%{link_end} allows having more users, and additional users will incur a retroactive charge on renewal."
msgstr ""
@@ -39650,9 +39665,6 @@ msgstr ""
msgid "This will invalidate your registered applications and U2F devices."
msgstr ""
-msgid "This will redirect you to an external sign in page."
-msgstr ""
-
msgid "This will remove the fork relationship between this project and %{fork_source}."
msgstr ""
@@ -45216,6 +45228,9 @@ msgstr ""
msgid "closed"
msgstr ""
+msgid "closed %{timeago}"
+msgstr ""
+
msgid "closed issue"
msgstr ""
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index a0bebf6bd7a..98e22890cc5 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -318,11 +318,15 @@ module QA
end
def merge_immediately!
- if has_element?(:merge_moment_dropdown)
- click_element(:merge_moment_dropdown, skip_finished_loading_check: true)
- click_element(:merge_immediately_menu_item, skip_finished_loading_check: true)
- else
- click_element(:merge_button, skip_finished_loading_check: true)
+ retry_until(reload: true, sleep_interval: 1, max_attempts: 12) do
+ if has_element?(:merge_moment_dropdown)
+ click_element(:merge_moment_dropdown, skip_finished_loading_check: true)
+ click_element(:merge_immediately_menu_item, skip_finished_loading_check: true)
+ else
+ click_element(:merge_button, skip_finished_loading_check: true)
+ end
+
+ merged?
end
end
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index e85f5b7a972..1d2f1085d3c 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -27,6 +27,18 @@ RSpec.describe GraphqlController do
)
end
+ it 'handles a timeout nicely' do
+ allow(subject).to receive(:execute) do
+ raise ActiveRecord::QueryCanceled, '**taps wristwatch**'
+ end
+
+ post :execute
+
+ expect(json_response).to include(
+ 'errors' => include(a_hash_including('message' => /Request timed out/))
+ )
+ end
+
it 'handles StandardError' do
allow(subject).to receive(:execute) do
raise StandardError, message
diff --git a/spec/events/pages/page_deployed_event_spec.rb b/spec/events/pages/page_deployed_event_spec.rb
new file mode 100644
index 00000000000..0c33a95b281
--- /dev/null
+++ b/spec/events/pages/page_deployed_event_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Pages::PageDeployedEvent do
+ where(:data, :valid) do
+ [
+ [{ project_id: 1, namespace_id: 2, root_namespace_id: 3 }, true],
+ [{ project_id: 1 }, false],
+ [{ namespace_id: 1 }, false],
+ [{ project_id: 'foo', namespace_id: 2 }, false],
+ [{ project_id: 1, namespace_id: 'foo' }, false],
+ [{ project_id: [], namespace_id: 2 }, false],
+ [{ project_id: 1, namespace_id: [] }, false],
+ [{ project_id: {}, namespace_id: 2 }, false],
+ [{ project_id: 1, namespace_id: {} }, false],
+ ['foo', false],
+ [123, false],
+ [[], false]
+ ]
+ end
+
+ with_them do
+ it 'validates data' do
+ constructor = -> { described_class.new(data: data) }
+
+ if valid
+ expect { constructor.call }.not_to raise_error
+ else
+ expect { constructor.call }.to raise_error(Gitlab::EventStore::InvalidEvent)
+ end
+ end
+ end
+end
diff --git a/spec/features/admin/admin_sees_background_migrations_spec.rb b/spec/features/admin/admin_sees_background_migrations_spec.rb
index 8edddcf9a9b..faf13374719 100644
--- a/spec/features/admin/admin_sees_background_migrations_spec.rb
+++ b/spec/features/admin/admin_sees_background_migrations_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe "Admin > Admin sees background migrations" do
let_it_be(:admin) { create(:admin) }
+ let(:job_class) { Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob }
let_it_be(:active_migration) { create(:batched_background_migration, :active, table_name: 'active') }
let_it_be(:failed_migration) { create(:batched_background_migration, :failed, table_name: 'failed', total_tuple_count: 100) }
@@ -107,7 +108,8 @@ RSpec.describe "Admin > Admin sees background migrations" do
anything,
batch_min_value: 6,
batch_size: 5,
- job_arguments: failed_migration.job_arguments
+ job_arguments: failed_migration.job_arguments,
+ job_class: job_class
).and_return([6, 10])
end
end
diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb
index ee1daf69f62..86cc761d790 100644
--- a/spec/features/profiles/oauth_applications_spec.rb
+++ b/spec/features/profiles/oauth_applications_spec.rb
@@ -35,9 +35,59 @@ RSpec.describe 'Profile > Applications' do
expect(page).to have_content('Your applications (0)')
expect(page).to have_content('Authorized applications (0)')
end
+ end
+
+ describe 'Authorized applications', :js do
+ let(:other_user) { create(:user) }
+ let(:application) { create(:oauth_application, owner: user) }
+ let(:created_at) { 2.days.ago }
+ let(:token) { create(:oauth_access_token, application: application, resource_owner: user) }
+ let(:anonymous_token) { create(:oauth_access_token, resource_owner: user) }
+
+ context 'with multiple access token types and multiple owners' do
+ let!(:other_user_token) { create(:oauth_access_token, application: application, resource_owner: other_user) }
+
+ before do
+ token.update_column(:created_at, created_at)
+ anonymous_token.update_columns(application_id: nil, created_at: 1.day.ago)
+ end
+
+ it 'displays the correct authorized applications' do
+ visit oauth_applications_path
+
+ expect(page).to have_content('Authorized applications (2)')
+
+ page.within('div.oauth-authorized-applications') do
+ # Ensure the correct user's token details are displayed
+ # when the application has more than one token
+ page.within("tr#application_#{application.id}") do
+ expect(page).to have_content(created_at)
+ end
+
+ expect(page).to have_content('Anonymous')
+ expect(page).not_to have_content(other_user_token.created_at)
+ end
+ end
+ end
it 'deletes an authorized application' do
- create(:oauth_access_token, resource_owner: user)
+ token
+ visit oauth_applications_path
+
+ page.within('div.oauth-authorized-applications') do
+ page.within("tr#application_#{application.id}") do
+ click_button 'Revoke'
+ end
+ end
+
+ accept_gl_confirm(button_text: 'Revoke application')
+
+ expect(page).to have_content('The application was revoked access.')
+ expect(page).to have_content('Authorized applications (0)')
+ end
+
+ it 'deletes an anonymous authorized application' do
+ anonymous_token
visit oauth_applications_path
page.within('.oauth-authorized-applications') do
@@ -48,7 +98,6 @@ RSpec.describe 'Profile > Applications' do
accept_gl_confirm(button_text: 'Revoke application')
expect(page).to have_content('The application was revoked access.')
- expect(page).to have_content('Your applications (0)')
expect(page).to have_content('Authorized applications (0)')
end
end
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index 42f2d08082e..4347c580a4d 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -32,6 +32,7 @@ export const getIssuesQueryResponse = {
state: 'opened',
title: 'Issue title',
updatedAt: '2021-05-22T04:08:01Z',
+ closedAt: null,
upvotes: 3,
userDiscussionsCount: 4,
webPath: 'project/-/issues/789',
diff --git a/spec/frontend/issues/list/utils_spec.js b/spec/frontend/issues/list/utils_spec.js
index e8ffba9bc80..90eab1f3754 100644
--- a/spec/frontend/issues/list/utils_spec.js
+++ b/spec/frontend/issues/list/utils_spec.js
@@ -97,10 +97,10 @@ describe('isSortKey', () => {
describe('getSortOptions', () => {
describe.each`
hasIssueWeightsFeature | hasBlockedIssuesFeature | length | containsWeight | containsBlocking
- ${false} | ${false} | ${9} | ${false} | ${false}
- ${true} | ${false} | ${10} | ${true} | ${false}
- ${false} | ${true} | ${10} | ${false} | ${true}
- ${true} | ${true} | ${11} | ${true} | ${true}
+ ${false} | ${false} | ${10} | ${false} | ${false}
+ ${true} | ${false} | ${11} | ${true} | ${false}
+ ${false} | ${true} | ${11} | ${false} | ${true}
+ ${true} | ${true} | ${12} | ${true} | ${true}
`(
'when hasIssueWeightsFeature=$hasIssueWeightsFeature and hasBlockedIssuesFeature=$hasBlockedIssuesFeature',
({
diff --git a/spec/frontend/repository/commits_service_spec.js b/spec/frontend/repository/commits_service_spec.js
index 697fa7c4fd1..71255d1d6e9 100644
--- a/spec/frontend/repository/commits_service_spec.js
+++ b/spec/frontend/repository/commits_service_spec.js
@@ -39,10 +39,11 @@ describe('commits service', () => {
expect(axios.get).toHaveBeenCalledWith(testUrl, { params: { format: 'json', offset } });
});
- it('encodes the path correctly', async () => {
- await requestCommits(1, 'some-project', 'with $peci@l ch@rs/');
+ it('encodes the path and ref correctly', async () => {
+ await requestCommits(1, 'some-project', 'with $peci@l ch@rs/', 'r€f-#');
- const encodedUrl = '/some-project/-/refs/main/logs_tree/with%20%24peci%40l%20ch%40rs%2F';
+ const encodedUrl =
+ '/some-project/-/refs/r%E2%82%ACf-%23/logs_tree/with%20%24peci%40l%20ch%40rs%2F';
expect(axios.get).toHaveBeenCalledWith(encodedUrl, expect.anything());
});
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
index ff0371b5c07..36adf3aea03 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -11,7 +11,7 @@ const MOCK_BLOBS = [
{
id: '123abc',
sha: '123abc',
- flatPath: 'blob',
+ flatPath: 'main/blob.md',
name: 'blob.md',
type: 'blob',
webPath: '/blob',
@@ -19,7 +19,7 @@ const MOCK_BLOBS = [
{
id: '124abc',
sha: '124abc',
- flatPath: 'blob2',
+ flatPath: 'main/blob2.md',
name: 'blob2.md',
type: 'blob',
webUrl: 'http://test.com',
@@ -27,7 +27,7 @@ const MOCK_BLOBS = [
{
id: '125abc',
sha: '125abc',
- flatPath: 'blob3',
+ flatPath: 'main/blob3.md',
name: 'blob3.md',
type: 'blob',
webUrl: 'http://test.com',
@@ -37,21 +37,21 @@ const MOCK_BLOBS = [
const MOCK_COMMITS = [
{
- fileName: 'blob.md',
+ filePath: 'main/blob.md',
type: 'blob',
commit: {
message: 'Updated blob.md',
},
},
{
- fileName: 'blob2.md',
+ filePath: 'main/blob2.md',
type: 'blob',
commit: {
message: 'Updated blob2.md',
},
},
{
- fileName: 'blob3.md',
+ filePath: 'main/blob3.md',
type: 'blob',
commit: {
message: 'Updated blob3.md',
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index 6abbb052aef..a41b52a597e 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -32,6 +32,7 @@ import {
fullReportExtension,
noTelemetryExtension,
pollingExtension,
+ pollingFullDataExtension,
pollingErrorExtension,
multiPollingExtension,
} from './test_extensions';
@@ -1082,6 +1083,37 @@ describe('MrWidgetOptions', () => {
});
});
+ describe('success - full data polling', () => {
+ it('sets data when polling is complete', async () => {
+ registerExtension(pollingFullDataExtension);
+
+ createComponent();
+
+ await waitForPromises();
+
+ api.trackRedisHllUserEvent.mockClear();
+ api.trackRedisCounterEvent.mockClear();
+
+ findExtensionToggleButton().trigger('click');
+
+ // The default working extension is a "warning" type, which generates a second - more specific - telemetry event for expansions
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledTimes(2);
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
+ 'i_merge_request_widget_test_extension_expand',
+ );
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
+ 'i_merge_request_widget_test_extension_expand_warning',
+ );
+ expect(api.trackRedisCounterEvent).toHaveBeenCalledTimes(2);
+ expect(api.trackRedisCounterEvent).toHaveBeenCalledWith(
+ 'i_merge_request_widget_test_extension_count_expand',
+ );
+ expect(api.trackRedisCounterEvent).toHaveBeenCalledWith(
+ 'i_merge_request_widget_test_extension_count_expand_warning',
+ );
+ });
+ });
+
describe('error', () => {
let captureException;
diff --git a/spec/frontend/vue_mr_widget/test_extensions.js b/spec/frontend/vue_mr_widget/test_extensions.js
index 76644e0be77..1977f550577 100644
--- a/spec/frontend/vue_mr_widget/test_extensions.js
+++ b/spec/frontend/vue_mr_widget/test_extensions.js
@@ -109,6 +109,39 @@ export const pollingExtension = {
enablePolling: true,
};
+export const pollingFullDataExtension = {
+ ...workingExtension(),
+ enableExpandedPolling: true,
+ methods: {
+ fetchCollapsedData({ targetProjectFullPath }) {
+ return Promise.resolve({ targetProjectFullPath, count: 1 });
+ },
+ fetchFullData() {
+ return Promise.resolve([
+ {
+ headers: { 'poll-interval': 0 },
+ status: 200,
+ data: {
+ id: 1,
+ text: 'Hello world',
+ icon: {
+ name: EXTENSION_ICONS.failed,
+ },
+ badge: {
+ text: 'Closed',
+ },
+ link: {
+ href: 'https://gitlab.com',
+ text: 'GitLab.com',
+ },
+ actions: [{ text: 'Full report', href: 'https://gitlab.com', target: '_blank' }],
+ },
+ },
+ ]);
+ },
+ },
+};
+
export const fullReportExtension = {
...workingExtension(),
computed: {
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
index 70017903079..bb8c5dedddd 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
@@ -39,6 +39,8 @@ describe('IssuableItem', () => {
const originalUrl = gon.gitlab_url;
let wrapper;
+ const findTimestampWrapper = () => wrapper.find('[data-testid="issuable-timestamp"]');
+
beforeEach(() => {
gon.gitlab_url = MOCK_GITLAB_URL;
});
@@ -150,12 +152,37 @@ describe('IssuableItem', () => {
});
});
- describe('updatedAt', () => {
- it('returns string containing timeago string based on `issuable.updatedAt`', () => {
+ describe('timestamp', () => {
+ it('returns string containing date and time based on `issuable.updatedAt` when the issue is open', () => {
+ wrapper = createComponent();
+
+ expect(findTimestampWrapper().attributes('title')).toBe('Sep 10, 2020 11:41am UTC');
+ });
+
+ it('returns string containing timeago string based on `issuable.closedAt` when the issue is closed', () => {
+ wrapper = createComponent({
+ issuable: { ...mockIssuable, closedAt: '2020-06-18T11:30:00Z', state: 'closed' },
+ });
+
+ expect(findTimestampWrapper().attributes('title')).toBe('Jun 18, 2020 11:30am UTC');
+ });
+ });
+
+ describe('formattedTimestamp', () => {
+ it('returns string containing timeago string based on `issuable.updatedAt` when the issue is open', () => {
wrapper = createComponent();
- expect(wrapper.vm.updatedAt).toContain('updated');
- expect(wrapper.vm.updatedAt).toContain('ago');
+ expect(findTimestampWrapper().text()).toContain('updated');
+ expect(findTimestampWrapper().text()).toContain('ago');
+ });
+
+ it('returns string containing timeago string based on `issuable.closedAt` when the issue is closed', () => {
+ wrapper = createComponent({
+ issuable: { ...mockIssuable, closedAt: '2020-06-18T11:30:00Z', state: 'closed' },
+ });
+
+ expect(findTimestampWrapper().text()).toContain('closed');
+ expect(findTimestampWrapper().text()).toContain('ago');
});
});
@@ -456,18 +483,31 @@ describe('IssuableItem', () => {
it('renders issuable updatedAt info', () => {
wrapper = createComponent();
- const updatedAtEl = wrapper.find('[data-testid="issuable-updated-at"]');
+ const timestampEl = wrapper.find('[data-testid="issuable-timestamp"]');
- expect(updatedAtEl.attributes('title')).toBe('Sep 10, 2020 11:41am UTC');
- expect(updatedAtEl.text()).toBe(wrapper.vm.updatedAt);
+ expect(timestampEl.attributes('title')).toBe('Sep 10, 2020 11:41am UTC');
+ expect(timestampEl.text()).toBe(wrapper.vm.formattedTimestamp);
});
describe('when issuable is closed', () => {
it('renders issuable card with a closed style', () => {
- wrapper = createComponent({ issuable: { ...mockIssuable, closedAt: '2020-12-10' } });
+ wrapper = createComponent({
+ issuable: { ...mockIssuable, closedAt: '2020-12-10', state: 'closed' },
+ });
expect(wrapper.classes()).toContain('closed');
});
+
+ it('renders issuable closedAt info and does not render updatedAt info', () => {
+ wrapper = createComponent({
+ issuable: { ...mockIssuable, closedAt: '2022-06-18T11:30:00Z', state: 'closed' },
+ });
+
+ const timestampEl = wrapper.find('[data-testid="issuable-timestamp"]');
+
+ expect(timestampEl.attributes('title')).toBe('Jun 18, 2022 11:30am UTC');
+ expect(timestampEl.text()).toBe(wrapper.vm.formattedTimestamp);
+ });
});
describe('when issuable was created within the past 24 hours', () => {
diff --git a/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb b/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb
new file mode 100644
index 00000000000..7138578f308
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_imported_issue_search_data_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillImportedIssueSearchData, :migration, schema: 20220621040800 do
+ let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let!(:issue_search_data_table) { table(:issue_search_data) }
+
+ let!(:user) { table(:users).create!(email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let!(:project) do
+ table(:projects)
+ .create!(
+ namespace_id: namespace.id,
+ creator_id: user.id,
+ name: 'projecty',
+ path: 'path',
+ project_namespace_id: namespace.id)
+ end
+
+ let!(:issue) do
+ table(:issues).create!(
+ project_id: project.id,
+ title: 'Patterson',
+ description: FFaker::HipsterIpsum.paragraph
+ )
+ end
+
+ let(:migration) do
+ described_class.new(start_id: 1,
+ end_id: 30,
+ batch_table: :issues,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection)
+ end
+
+ let(:perform_migration) { migration.perform }
+
+ context 'when issue has search data record' do
+ let!(:issue_search_data) { issue_search_data_table.create!(project_id: project.id, issue_id: issue.id) }
+
+ it 'does not create or update any search data records' do
+ expect { perform_migration }
+ .to not_change { issue_search_data_table.count }
+ .and not_change { issue_search_data }
+
+ expect(issue_search_data_table.count).to eq(1)
+ end
+ end
+
+ context 'when issue has no search data record' do
+ let(:title_node) { "'#{issue.title.downcase}':1A" }
+
+ it 'creates search data records' do
+ expect { perform_migration }
+ .to change { issue_search_data_table.count }.from(0).to(1)
+
+ expect(issue_search_data_table.find_by(project_id: project.id).issue_id)
+ .to eq(issue.id)
+
+ expect(issue_search_data_table.find_by(project_id: project.id).search_vector)
+ .to include(title_node)
+ end
+ end
+
+ context 'error handling' do
+ let!(:issue2) do
+ table(:issues).create!(
+ project_id: project.id,
+ title: 'Chatterton',
+ description: FFaker::HipsterIpsum.paragraph
+ )
+ end
+
+ before do
+ issue.update!(description: Array.new(30_000) { SecureRandom.hex }.join(' '))
+ end
+
+ let(:title_node2) { "'#{issue2.title.downcase}':1A" }
+
+ it 'skips insertion for that issue but continues with migration' do
+ expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |logger|
+ expect(logger)
+ .to receive(:error)
+ .with(a_hash_including(message: /string is too long for tsvector/, model_id: issue.id))
+ end
+
+ expect { perform_migration }.to change { issue_search_data_table.count }.from(0).to(1)
+ expect(issue_search_data_table.find_by(issue_id: issue.id)).to eq(nil)
+ expect(issue_search_data_table.find_by(issue_id: issue2.id).search_vector)
+ .to include(title_node2)
+ end
+
+ it 're-raises exceptions' do
+ allow(migration)
+ .to receive(:update_search_data_individually)
+ .and_raise(ActiveRecord::StatementTimeout)
+
+ expect { perform_migration }.to raise_error(ActiveRecord::StatementTimeout)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb
index 521e2067744..943b5744b64 100644
--- a/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb
+++ b/spec/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy_spec.rb
@@ -45,10 +45,30 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchi
end
end
+ context 'when job_class is provided with a batching_scope' do
+ let(:job_class) do
+ Class.new(described_class) do
+ def self.batching_scope(relation, job_arguments:)
+ min_id = job_arguments.first
+
+ relation.where.not(type: 'Project').where('id >= ?', min_id)
+ end
+ end
+ end
+
+ it 'applies the batching scope' do
+ expect(job_class).to receive(:batching_scope).and_call_original
+
+ batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace4.id, batch_size: 3, job_arguments: [1], job_class: job_class)
+
+ expect(batch_bounds).to eq([namespace4.id, namespace4.id])
+ end
+ end
+
context 'additional filters' do
let(:strategy_with_filters) do
Class.new(described_class) do
- def apply_additional_filters(relation, job_arguments:)
+ def apply_additional_filters(relation, job_arguments:, job_class: nil)
min_id = job_arguments.first
relation.where.not(type: 'Project').where('id >= ?', min_id)
diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
index 9057c4e99df..7b97dde3808 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
@@ -77,7 +77,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Create do
context 'without tags' do
it 'extracts an empty tag list' do
- expect(CommitStatus)
+ expect(Gitlab::Ci::Tags::BulkInsert)
.to receive(:bulk_insert_tags!)
.with([job])
.and_call_original
@@ -95,7 +95,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Create do
end
it 'bulk inserts tags' do
- expect(CommitStatus)
+ expect(Gitlab::Ci::Tags::BulkInsert)
.to receive(:bulk_insert_tags!)
.with([job])
.and_call_original
diff --git a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb
index 063376499e2..72574d50176 100644
--- a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb
+++ b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Gitlab::Ci::Tags::BulkInsert do
let(:error_message) do
<<~MESSAGE
A mechanism depending on internals of 'act-as-taggable-on` has been designed
- to bulk insert tags for Ci::Build records.
+ to bulk insert tags for Ci::Build/Ci::Runner records.
Please review the code carefully before updating the gem version
https://gitlab.com/gitlab-org/gitlab/-/issues/350053
MESSAGE
@@ -27,6 +27,21 @@ RSpec.describe Gitlab::Ci::Tags::BulkInsert do
it { expect(ActsAsTaggableOn::VERSION).to eq(acceptable_version), error_message }
end
+ describe '.bulk_insert_tags!' do
+ let(:inserter) { instance_double(described_class) }
+
+ it 'delegates to bulk insert class' do
+ expect(Gitlab::Ci::Tags::BulkInsert)
+ .to receive(:new)
+ .with(statuses)
+ .and_return(inserter)
+
+ expect(inserter).to receive(:insert!)
+
+ described_class.bulk_insert_tags!(statuses)
+ end
+ end
+
describe '#insert!' do
context 'without tags' do
it { expect(service.insert!).to be_falsey }
@@ -45,6 +60,17 @@ RSpec.describe Gitlab::Ci::Tags::BulkInsert do
expect(other_job.reload.tag_list).to match_array(%w[tag2 tag3 tag4])
end
+ it 'persists taggings' do
+ service.insert!
+
+ expect(job.taggings.size).to eq(2)
+ expect(other_job.taggings.size).to eq(3)
+
+ expect(Ci::Build.tagged_with('tag1')).to include(job)
+ expect(Ci::Build.tagged_with('tag2')).to include(job, other_job)
+ expect(Ci::Build.tagged_with('tag3')).to include(other_job)
+ end
+
context 'when batching inserts for tags' do
before do
stub_const("#{described_class}::TAGS_BATCH_SIZE", 2)
@@ -83,6 +109,15 @@ RSpec.describe Gitlab::Ci::Tags::BulkInsert do
expect(job.reload.tag_list).to match_array(%w[tag1 tag2])
expect(other_job.reload.tag_list).to be_empty
end
+
+ it 'persists taggings' do
+ service.insert!
+
+ expect(job.taggings.size).to eq(2)
+
+ expect(Ci::Build.tagged_with('tag1')).to include(job)
+ expect(Ci::Build.tagged_with('tag2')).to include(job)
+ 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
index 8819171cfd0..6fe3b22d8bc 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
@@ -322,6 +322,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
describe '#retry_failed_jobs!' do
let(:batched_migration) { create(:batched_background_migration, status: 'failed') }
+ let(:job_class) { Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob }
subject(:retry_failed_jobs) { batched_migration.retry_failed_jobs! }
@@ -335,7 +336,8 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
anything,
batch_min_value: 6,
batch_size: 5,
- job_arguments: batched_migration.job_arguments
+ job_arguments: batched_migration.job_arguments,
+ job_class: job_class
).and_return([6, 10])
end
end
diff --git a/spec/migrations/20220628012902_finalise_project_namespace_members_spec.rb b/spec/migrations/20220628012902_finalise_project_namespace_members_spec.rb
new file mode 100644
index 00000000000..1f116cf6a7e
--- /dev/null
+++ b/spec/migrations/20220628012902_finalise_project_namespace_members_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FinaliseProjectNamespaceMembers, :migration do
+ let(:batched_migrations) { table(:batched_background_migrations) }
+
+ let_it_be(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ shared_examples 'finalizes the migration' do
+ it 'finalizes the migration' do
+ allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner|
+ expect(runner).to receive(:finalize).with('BackfillProjectMemberNamespaceId', :members, :id, [])
+ end
+ end
+ end
+
+ context 'when migration is missing' do
+ it 'warns migration not found' do
+ expect(Gitlab::AppLogger)
+ .to receive(:warn).with(/Could not find batched background migration for the given configuration:/)
+
+ migrate!
+ end
+ end
+
+ context 'with migration present' do
+ let!(:project_member_namespace_id_backfill) do
+ batched_migrations.create!(
+ job_class_name: 'BackfillProjectMemberNamespaceId',
+ table_name: :members,
+ column_name: :id,
+ job_arguments: [],
+ interval: 2.minutes,
+ min_value: 1,
+ max_value: 2,
+ batch_size: 1000,
+ sub_batch_size: 200,
+ gitlab_schema: :gitlab_main,
+ status: 3 # finished
+ )
+ end
+
+ context 'when migration finished successfully' do
+ it 'does not raise exception' do
+ expect { migrate! }.not_to raise_error
+ end
+ end
+
+ context 'with different migration statuses' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :description) do
+ 0 | 'paused'
+ 1 | 'active'
+ 4 | 'failed'
+ 5 | 'finalizing'
+ end
+
+ with_them do
+ before do
+ project_member_namespace_id_backfill.update!(status: status)
+ end
+
+ it_behaves_like 'finalizes the migration'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/backfill_imported_issue_search_data_spec.rb b/spec/migrations/backfill_imported_issue_search_data_spec.rb
new file mode 100644
index 00000000000..0b4b1aabd8e
--- /dev/null
+++ b/spec/migrations/backfill_imported_issue_search_data_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillImportedIssueSearchData do
+ let_it_be(:batched_migration) { described_class::MIGRATION }
+
+ context 'when BackfillIssueSearchData.max_value is nil' do
+ it 'schedules a new batched migration with a default max_value' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :issues,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_min_value: described_class::BATCH_MIN_VALUE
+ )
+ }
+ end
+ end
+ end
+
+ context 'when BackfillIssueSearchData.max_value exists' do
+ before do
+ Gitlab::Database::BackgroundMigration::BatchedMigration
+ .create!(
+ max_value: 200,
+ batch_size: 200,
+ sub_batch_size: 20,
+ interval: 120,
+ job_class_name: 'BackfillIssueSearchData',
+ table_name: 'issues',
+ column_name: 'id',
+ gitlab_schema: 'glschema'
+ )
+ end
+
+ it 'schedules a new batched migration with a custom max_value' do
+ reversible_migration do |migration|
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :issues,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_min_value: 200
+ )
+ }
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 6456ad3c1ec..1ecf7e8b216 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -2083,6 +2083,27 @@ RSpec.describe Ci::Build do
end
end
+ describe '#save_tags' do
+ let(:build) { create(:ci_build, tag_list: ['tag']) }
+
+ it 'saves tags' do
+ build.save!
+
+ expect(build.tags.count).to eq(1)
+ expect(build.tags.first.name).to eq('tag')
+ end
+
+ context 'with BulkInsertableTags.with_bulk_insert_tags' do
+ it 'does not save_tags' do
+ Ci::BulkInsertableTags.with_bulk_insert_tags do
+ build.save!
+ end
+
+ expect(build.tags).to be_empty
+ end
+ end
+ end
+
describe '#has_tags?' do
context 'when build has tags' do
subject { create(:ci_build, tag_list: ['tag']) }
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 74d8b012b29..c5cb67929e2 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -1193,6 +1193,40 @@ RSpec.describe Ci::Runner do
end
end
+ describe '#save_tags' do
+ let(:runner) { build(:ci_runner, tag_list: ['tag']) }
+
+ it 'saves tags' do
+ runner.save!
+
+ expect(runner.tags.count).to eq(1)
+ expect(runner.tags.first.name).to eq('tag')
+ end
+
+ context 'with BulkInsertableTags.with_bulk_insert_tags' do
+ it 'does not save_tags' do
+ Ci::BulkInsertableTags.with_bulk_insert_tags do
+ runner.save!
+ end
+
+ expect(runner.tags).to be_empty
+ end
+
+ context 'over TAG_LIST_MAX_LENGTH' do
+ let(:tag_list) { (1..described_class::TAG_LIST_MAX_LENGTH + 1).map { |i| "tag#{i}" } }
+ let(:runner) { build(:ci_runner, tag_list: tag_list) }
+
+ it 'fails validation if over tag limit' do
+ Ci::BulkInsertableTags.with_bulk_insert_tags do
+ expect { runner.save! }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+
+ expect(runner.tags).to be_empty
+ end
+ end
+ end
+ end
+
describe '#has_tags?' do
context 'when runner has tags' do
subject { create(:ci_runner, tag_list: ['tag']) }
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index dbb15fad246..9f58d3c1a80 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -984,22 +984,6 @@ RSpec.describe CommitStatus do
end
end
- describe '.bulk_insert_tags!' do
- let(:statuses) { double('statuses') }
- let(:inserter) { double('inserter') }
-
- it 'delegates to bulk insert class' do
- expect(Gitlab::Ci::Tags::BulkInsert)
- .to receive(:new)
- .with(statuses)
- .and_return(inserter)
-
- expect(inserter).to receive(:insert!)
-
- described_class.bulk_insert_tags!(statuses)
- end
- end
-
describe '#expire_etag_cache!' do
it 'expires the etag cache' do
expect_next_instance_of(Gitlab::EtagCaching::Store) do |etag_store|
diff --git a/spec/models/concerns/ci/bulk_insertable_tags_spec.rb b/spec/models/concerns/ci/bulk_insertable_tags_spec.rb
new file mode 100644
index 00000000000..23f0831403d
--- /dev/null
+++ b/spec/models/concerns/ci/bulk_insertable_tags_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::BulkInsertableTags do
+ let(:taggable_class) do
+ Class.new do
+ prepend Ci::BulkInsertableTags
+
+ attr_reader :tags_saved
+
+ def save_tags
+ @tags_saved = true
+ end
+ end
+ end
+
+ let(:record) { taggable_class.new }
+
+ describe '.with_bulk_insert_tags' do
+ it 'changes the thread key to true' do
+ expect(Thread.current['ci_bulk_insert_tags']).to be_nil
+
+ described_class.with_bulk_insert_tags do
+ expect(Thread.current['ci_bulk_insert_tags']).to eq(true)
+ end
+
+ expect(Thread.current['ci_bulk_insert_tags']).to be_nil
+ end
+ end
+
+ describe '#save_tags' do
+ it 'calls super' do
+ record.save_tags
+
+ expect(record.tags_saved).to eq(true)
+ end
+
+ it 'does not call super with BulkInsertableTags.with_bulk_insert_tags' do
+ described_class.with_bulk_insert_tags do
+ record.save_tags
+ end
+
+ expect(record.tags_saved).to be_nil
+ end
+
+ it 'isolates bulk insert behavior between threads' do
+ record2 = taggable_class.new
+
+ t1 = Thread.new do
+ described_class.with_bulk_insert_tags do
+ record.save_tags
+ end
+ end
+
+ t2 = Thread.new do
+ record2.save_tags
+ end
+
+ [t1, t2].each(&:join)
+
+ expect(record.tags_saved).to be_nil
+ expect(record2.tags_saved).to eq(true)
+ end
+ end
+end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 381eccf2376..77b81b2c310 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -4903,7 +4903,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
.to delegate_method(:builds_with_coverage)
.to(:head_pipeline)
.with_prefix
- .with_arguments(allow_nil: true)
+ .allow_nil
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 96e06e617d5..ee21b857fc8 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -337,16 +337,13 @@ RSpec.describe Namespace do
end
describe 'delegate' do
- it { is_expected.to delegate_method(:name).to(:owner).with_prefix.with_arguments(allow_nil: true) }
- it { is_expected.to delegate_method(:avatar_url).to(:owner).with_arguments(allow_nil: true) }
- it do
- is_expected.to delegate_method(:prevent_sharing_groups_outside_hierarchy)
- .to(:namespace_settings).with_arguments(allow_nil: true)
- end
+ it { is_expected.to delegate_method(:name).to(:owner).with_prefix.allow_nil }
+ it { is_expected.to delegate_method(:avatar_url).to(:owner).allow_nil }
+ it { is_expected.to delegate_method(:prevent_sharing_groups_outside_hierarchy).to(:namespace_settings).allow_nil }
it do
- is_expected.to delegate_method(:prevent_sharing_groups_outside_hierarchy=)
- .to(:namespace_settings).with_arguments(allow_nil: true)
+ is_expected.to delegate_method(:prevent_sharing_groups_outside_hierarchy=).to(:namespace_settings)
+ .with_arguments(:args).allow_nil
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 80a37ff0d80..421e9ec47c3 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -826,26 +826,33 @@ RSpec.describe Project, factory_default: :keep do
end
it { is_expected.to delegate_method(:members).to(:team).with_prefix(true) }
- it { is_expected.to delegate_method(:name).to(:owner).with_prefix(true).with_arguments(allow_nil: true) }
- it { is_expected.to delegate_method(:root_ancestor).to(:namespace).with_arguments(allow_nil: true) }
- it { is_expected.to delegate_method(:certificate_based_clusters_enabled?).to(:namespace).with_arguments(allow_nil: true) }
- it { is_expected.to delegate_method(:last_pipeline).to(:commit).with_arguments(allow_nil: true) }
+ it { is_expected.to delegate_method(:name).to(:owner).with_prefix(true).allow_nil }
+ it { is_expected.to delegate_method(:root_ancestor).to(:namespace).allow_nil }
+ it { is_expected.to delegate_method(:certificate_based_clusters_enabled?).to(:namespace).allow_nil }
+ it { is_expected.to delegate_method(:last_pipeline).to(:commit).allow_nil }
it { is_expected.to delegate_method(:container_registry_enabled?).to(:project_feature) }
it { is_expected.to delegate_method(:container_registry_access_level).to(:project_feature) }
- describe 'project settings' do
+ describe 'read project settings' do
%i(
show_default_award_emojis
- show_default_award_emojis=
show_default_award_emojis?
warn_about_potentially_unwanted_characters
- warn_about_potentially_unwanted_characters=
warn_about_potentially_unwanted_characters?
enforce_auth_checks_on_uploads
- enforce_auth_checks_on_uploads=
enforce_auth_checks_on_uploads?
).each do |method|
- it { is_expected.to delegate_method(method).to(:project_setting).with_arguments(allow_nil: true) }
+ it { is_expected.to delegate_method(method).to(:project_setting).allow_nil }
+ end
+ end
+
+ describe 'write project settings' do
+ %i(
+ show_default_award_emojis=
+ warn_about_potentially_unwanted_characters=
+ enforce_auth_checks_on_uploads=
+ ).each do |method|
+ it { is_expected.to delegate_method(method).to(:project_setting).with_arguments(:args).allow_nil }
end
end
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index 7b7463e6abc..498b2a32a0e 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -106,7 +106,7 @@ RSpec.describe BlobPresenter do
end
describe '#find_file_path' do
- it { expect(presenter.find_file_path).to eq("/#{project.full_path}/-/find_file/HEAD/files/ruby/regex.rb") }
+ it { expect(presenter.find_file_path).to eq("/#{project.full_path}/-/find_file/HEAD") }
end
describe '#blame_path' do
diff --git a/spec/requests/admin/background_migrations_controller_spec.rb b/spec/requests/admin/background_migrations_controller_spec.rb
index 884448fdd95..fe2a2470511 100644
--- a/spec/requests/admin/background_migrations_controller_spec.rb
+++ b/spec/requests/admin/background_migrations_controller_spec.rb
@@ -97,6 +97,7 @@ RSpec.describe Admin::BackgroundMigrationsController, :enable_admin_mode do
describe 'POST #retry' do
let(:migration) { create(:batched_background_migration, :failed) }
+ let(:job_class) { Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob }
before do
create(:batched_background_migration_job, :failed, batched_migration: migration, batch_size: 10, min_value: 6, max_value: 15, attempts: 3)
@@ -107,7 +108,8 @@ RSpec.describe Admin::BackgroundMigrationsController, :enable_admin_mode do
anything,
batch_min_value: 6,
batch_size: 5,
- job_arguments: migration.job_arguments
+ job_arguments: migration.job_arguments,
+ job_class: job_class
).and_return([6, 10])
end
end
diff --git a/spec/services/ci/runners/register_runner_service_spec.rb b/spec/services/ci/runners/register_runner_service_spec.rb
index f43fd823078..03dcf851e53 100644
--- a/spec/services/ci/runners/register_runner_service_spec.rb
+++ b/spec/services/ci/runners/register_runner_service_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do
stub_application_setting(valid_runner_registrars: ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES)
end
- subject { described_class.new.execute(token, args) }
+ subject(:runner) { described_class.new.execute(token, args) }
context 'when no token is provided' do
let(:token) { '' }
@@ -83,6 +83,9 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do
expect(subject.platform).to eq args[:platform]
expect(subject.architecture).to eq args[:architecture]
expect(subject.ip_address).to eq args[:ip_address]
+
+ expect(Ci::Runner.tagged_with('tag1')).to include(subject)
+ expect(Ci::Runner.tagged_with('tag2')).to include(subject)
end
end
@@ -230,5 +233,41 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do
end
end
end
+
+ context 'when tags are provided' do
+ let(:token) { registration_token }
+
+ let(:args) do
+ { tag_list: %w(tag1 tag2) }
+ end
+
+ it 'creates runner with tags' do
+ expect(runner).to be_persisted
+
+ expect(runner.tags).to contain_exactly(
+ an_object_having_attributes(name: 'tag1'),
+ an_object_having_attributes(name: 'tag2')
+ )
+ end
+
+ it 'creates tags in bulk' do
+ expect(Gitlab::Ci::Tags::BulkInsert).to receive(:bulk_insert_tags!).and_call_original
+
+ expect(runner).to be_persisted
+ end
+
+ context 'and tag list exceeds limit' do
+ let(:args) do
+ { tag_list: (1..Ci::Runner::TAG_LIST_MAX_LENGTH + 1).map { |i| "tag#{i}" } }
+ end
+
+ it 'does not create any tags' do
+ expect(Gitlab::Ci::Tags::BulkInsert).not_to receive(:bulk_insert_tags!)
+
+ expect(runner).not_to be_persisted
+ expect(runner.tags).to be_empty
+ end
+ end
+ end
end
end
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index cbbed82aa0b..06e39ccc8c7 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -43,6 +43,16 @@ RSpec.describe Projects::UpdatePagesService do
expect(project.pages_deployed?).to be_truthy
end
+ it 'publishes a PageDeployedEvent event with project id and namespace id' do
+ expected_data = {
+ project_id: project.id,
+ namespace_id: project.namespace_id,
+ root_namespace_id: project.root_namespace.id
+ }
+
+ expect { subject.execute }.to publish_event(Pages::PageDeployedEvent).with(expected_data)
+ end
+
it 'creates pages_deployment and saves it in the metadata' do
expect do
expect(execute).to eq(:success)
diff --git a/spec/support/matchers/background_migrations_matchers.rb b/spec/support/matchers/background_migrations_matchers.rb
index c5b3e140585..9f39f576b95 100644
--- a/spec/support/matchers/background_migrations_matchers.rb
+++ b/spec/support/matchers/background_migrations_matchers.rb
@@ -74,6 +74,13 @@ RSpec::Matchers.define :have_scheduled_batched_migration do |gitlab_schema: :git
.for_configuration(gitlab_schema, migration, table_name, column_name, job_arguments)
expect(batched_migrations.count).to be(1)
+
+ # the :batch_min_value & :batch_max_value attribute argument values get applied to the
+ # :min_value & :max_value columns on the database. Here we change the attribute names
+ # for the rspec have_attributes matcher used below to pass
+ attributes[:min_value] = attributes.delete :batch_min_value if attributes.include?(:batch_min_value)
+ attributes[:max_value] = attributes.delete :batch_max_value if attributes.include?(:batch_max_value)
+
expect(batched_migrations).to all(have_attributes(attributes)) if attributes.present?
end
diff --git a/workhorse/internal/upstream/upstream.go b/workhorse/internal/upstream/upstream.go
index 6d107fc28cd..fb511b5d456 100644
--- a/workhorse/internal/upstream/upstream.go
+++ b/workhorse/internal/upstream/upstream.go
@@ -217,19 +217,18 @@ func (u *upstream) pollGeoProxyAPI() {
func (u *upstream) callGeoProxyAPI() {
geoProxyData, err := u.APIClient.GetGeoProxyData()
if err != nil {
- log.WithError(err).WithFields(log.Fields{"geoProxyBackend": u.geoProxyBackend}).Error("Geo Proxy: Unable to determine Geo Proxy URL. Fallback on cached value.")
+ // Unable to determine Geo Proxy URL. Fallback on cached value.
return
}
hasProxyDataChanged := false
if u.geoProxyBackend.String() != geoProxyData.GeoProxyURL.String() {
- log.WithFields(log.Fields{"oldGeoProxyURL": u.geoProxyBackend, "newGeoProxyURL": geoProxyData.GeoProxyURL}).Info("Geo Proxy: URL changed")
+ // URL changed
hasProxyDataChanged = true
}
if u.geoProxyExtraData != geoProxyData.GeoProxyExtraData {
- // extra data is usually a JWT, thus not explicitly logging it
- log.Info("Geo Proxy: signed data changed")
+ // Signed data changed
hasProxyDataChanged = true
}