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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2024-01-12 21:08:59 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2024-01-12 21:08:59 +0300
commitbb5a73d8962c28abeef59ea3d6e90f4b2e370f36 (patch)
treef83de42bc46951514a3bda5f9b221a1be3acd72b
parent8b5595e9f1b9e46c97a69bfc0dc109658d5dbea2 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop.yml3
-rw-r--r--.rubocop_todo/rails/avoid_time_comparison.yml13
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue10
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue10
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_editable_item.vue33
-rw-r--r--app/models/namespace.rb1
-rw-r--r--app/models/namespaces/descendants.rb19
-rw-r--r--app/models/namespaces/traversal/cached.rb34
-rw-r--r--config/feature_flags/gitlab_com_derisk/namespace_descendants_cache_expiration.yml9
-rw-r--r--doc/api/graphql/reference/index.md62
-rw-r--r--doc/development/database_review.md2
-rw-r--r--lib/quality/seeders/issues.rb2
-rw-r--r--locale/gitlab.pot3
-rw-r--r--package.json6
-rw-r--r--rubocop/cop/rails/avoid_time_comparison.rb50
-rw-r--r--spec/models/namespaces/descendants_spec.rb25
-rw-r--r--spec/models/namespaces/traversal/cached_spec.rb104
-rw-r--r--spec/rubocop/cop/rails/avoid_time_comparison_spec.rb57
-rw-r--r--yarn.lock38
19 files changed, 456 insertions, 25 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 571946be462..d776a79a80d 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -375,6 +375,9 @@ Rails/HasManyOrHasOneDependent:
Rails/CreateTableWithTimestamps:
Enabled: false
+Rails/AvoidTimeComparison:
+ Enabled: true
+
# GitLab ###################################################################
Gitlab/ModuleWithInstanceVariables:
diff --git a/.rubocop_todo/rails/avoid_time_comparison.yml b/.rubocop_todo/rails/avoid_time_comparison.yml
new file mode 100644
index 00000000000..e6b6e9fadaf
--- /dev/null
+++ b/.rubocop_todo/rails/avoid_time_comparison.yml
@@ -0,0 +1,13 @@
+---
+Rails/AvoidTimeComparison:
+ Details: grace period
+ Exclude:
+ - 'app/services/packages/mark_package_files_for_destruction_service.rb'
+ - 'app/workers/container_registry/migration/enqueuer_worker.rb'
+ - 'app/workers/gitlab/import/advance_stage.rb'
+ - 'ee/app/services/incident_management/pending_escalations/process_service.rb'
+ - 'ee/app/workers/update_all_mirrors_worker.rb'
+ - 'lib/gitlab/chaos.rb'
+ - 'lib/gitlab/database/background_migration/batched_migration.rb'
+ - 'spec/lib/gitlab/ci/cron_parser_spec.rb'
+ - 'spec/support/helpers/wait_helpers.rb'
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
index 4ff12824008..0ac6208c7d3 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -6,6 +6,7 @@ import { TYPE_ALERT, TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import { __, n__ } from '~/locale';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { ISSUE_MR_CHANGE_ASSIGNEE } from '~/behaviors/shortcuts/keybindings';
import { assigneesQueries } from '../../queries/constants';
import SidebarEditableItem from '../sidebar_editable_item.vue';
import SidebarAssigneesRealtime from './assignees_realtime.vue';
@@ -156,6 +157,12 @@ export default {
issuableAuthor() {
return this.issuable?.author;
},
+ assigneeShortcutDescription() {
+ return ISSUE_MR_CHANGE_ASSIGNEE.description;
+ },
+ assigneeShortcutKey() {
+ return ISSUE_MR_CHANGE_ASSIGNEE.defaultKeys[0];
+ },
},
watch: {
iid(_, oldIid) {
@@ -246,6 +253,9 @@ export default {
:loading="isSettingAssignees"
:initial-loading="isAssigneesLoading"
:title="assigneeText"
+ :edit-tooltip="`${assigneeShortcutDescription} <kbd class='flat ml-1' aria-hidden=true>${assigneeShortcutKey}</kbd>`"
+ :edit-aria-label="assigneeShortcutDescription"
+ :edit-keyshortcuts="assigneeShortcutKey"
:is-dirty="isDirty"
@open="showDropdown"
@close="saveAssignees"
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
index e0d7400f7a6..686298753e2 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
@@ -7,6 +7,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { TYPE_EPIC, TYPE_ISSUE, TYPE_MERGE_REQUEST, TYPE_TEST_CASE } from '~/issues/constants';
import { __ } from '~/locale';
+import { ISSUABLE_CHANGE_LABEL } from '~/behaviors/shortcuts/keybindings';
import { issuableLabelsQueries } from '../../../queries/constants';
import SidebarEditableItem from '../../sidebar_editable_item.vue';
import { DEBOUNCE_DROPDOWN_DELAY, VARIANT_SIDEBAR } from './constants';
@@ -159,6 +160,12 @@ export default {
isLockOnMergeSupported() {
return this.issuableSupportsLockOnMerge || this.issuable?.supportsLockOnMerge;
},
+ labelShortcutDescription() {
+ return ISSUABLE_CHANGE_LABEL.description;
+ },
+ labelShortcutKey() {
+ return ISSUABLE_CHANGE_LABEL.defaultKeys[0];
+ },
},
apollo: {
issuable: {
@@ -375,6 +382,9 @@ export default {
<sidebar-editable-item
ref="editable"
:title="__('Labels')"
+ :edit-tooltip="`${labelShortcutDescription} <kbd class='flat ml-1' aria-hidden=true>${labelShortcutKey}</kbd>`"
+ :edit-aria-label="labelShortcutDescription"
+ :edit-keyshortcuts="labelShortcutKey"
:loading="isLoading"
:can-edit="allowLabelEdit"
@open="oldIid = null"
diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
index ad83866ceb2..c887d5d292e 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
@@ -7,6 +7,9 @@ export default {
unassigned: __('Unassigned'),
},
components: { GlButton, GlLoadingIcon },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
inject: {
canUpdate: {},
isClassicSidebar: {
@@ -58,6 +61,21 @@ export default {
required: false,
default: false,
},
+ editTooltip: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ editAriaLabel: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ editKeyshortcuts: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -68,6 +86,15 @@ export default {
editButtonText() {
return this.isDirty ? __('Apply') : __('Edit');
},
+ editTooltipText() {
+ return this.isDirty ? '' : this.editTooltip;
+ },
+ editAriaLabelText() {
+ return this.isDirty ? this.editButtonText : this.editAriaLabel;
+ },
+ editKeyshortcutsText() {
+ return this.isDirty ? __('Escape') : this.editKeyshortcuts;
+ },
},
destroyed() {
window.removeEventListener('click', this.collapseWhenOffClick);
@@ -150,9 +177,13 @@ export default {
<gl-button
v-if="canUpdate && !initialLoading && canEdit"
:id="buttonId"
+ v-gl-tooltip.viewport.html
category="tertiary"
size="small"
class="gl-text-gray-900! gl-ml-auto hide-collapsed gl-mr-n2 shortcut-sidebar-dropdown-toggle"
+ :title="editTooltipText"
+ :aria-label="editAriaLabelText"
+ :aria-keyshortcuts="editKeyshortcutsText"
data-testid="edit-button"
:data-track-action="tracking.event"
:data-track-label="tracking.label"
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index cc60a64bc9c..238556f0cf0 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -12,6 +12,7 @@ class Namespace < ApplicationRecord
include Gitlab::Utils::StrongMemoize
include Namespaces::Traversal::Recursive
include Namespaces::Traversal::Linear
+ include Namespaces::Traversal::Cached
include EachBatch
include BlocksUnsafeSerialization
include Ci::NamespaceSettings
diff --git a/app/models/namespaces/descendants.rb b/app/models/namespaces/descendants.rb
index 99abda2dd6a..8444cea9848 100644
--- a/app/models/namespaces/descendants.rb
+++ b/app/models/namespaces/descendants.rb
@@ -7,5 +7,24 @@ module Namespaces
belongs_to :namespace
validates :namespace_id, uniqueness: true
+
+ def self.expire_for(namespace_ids)
+ # Union:
+ # - Look up all parent ids including the given ids via traversal_ids
+ # - Include the given ids to handle the case when the namespaces records are already deleted
+ sql = <<~SQL
+ WITH namespace_ids AS MATERIALIZED (
+ (
+ SELECT ids.id
+ FROM namespaces, UNNEST(traversal_ids) ids(id)
+ WHERE namespaces.id IN (?)
+ ) UNION
+ (SELECT UNNEST(ARRAY[?]) AS id)
+ )
+ UPDATE namespace_descendants SET outdated_at = ? FROM namespace_ids WHERE namespace_descendants.namespace_id = namespace_ids.id
+ SQL
+
+ connection.execute(sanitize_sql_array([sql, namespace_ids, namespace_ids, Time.current]))
+ end
end
end
diff --git a/app/models/namespaces/traversal/cached.rb b/app/models/namespaces/traversal/cached.rb
new file mode 100644
index 00000000000..55eaaa4667e
--- /dev/null
+++ b/app/models/namespaces/traversal/cached.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Namespaces
+ module Traversal
+ module Cached
+ extend ActiveSupport::Concern
+ extend Gitlab::Utils::Override
+
+ included do
+ after_destroy :invalidate_descendants_cache
+ end
+
+ private
+
+ override :sync_traversal_ids
+ def sync_traversal_ids
+ super
+ return if is_a?(Namespaces::UserNamespace)
+ return unless Feature.enabled?(:namespace_descendants_cache_expiration, self, type: :gitlab_com_derisk)
+
+ ids = [id]
+ ids.concat((saved_changes[:parent_id] - [parent_id]).compact) if saved_changes[:parent_id]
+ Namespaces::Descendants.expire_for(ids)
+ end
+
+ def invalidate_descendants_cache
+ return if is_a?(Namespaces::UserNamespace)
+ return unless Feature.enabled?(:namespace_descendants_cache_expiration, self, type: :gitlab_com_derisk)
+
+ Namespaces::Descendants.expire_for([parent_id, id].compact)
+ end
+ end
+ end
+end
diff --git a/config/feature_flags/gitlab_com_derisk/namespace_descendants_cache_expiration.yml b/config/feature_flags/gitlab_com_derisk/namespace_descendants_cache_expiration.yml
new file mode 100644
index 00000000000..d374316e271
--- /dev/null
+++ b/config/feature_flags/gitlab_com_derisk/namespace_descendants_cache_expiration.yml
@@ -0,0 +1,9 @@
+---
+name: namespace_descendants_cache_expiration
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/433482
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/141588
+rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/production/-/issues/17388
+milestone: '16.8'
+group: group::optimize
+type: gitlab_com_derisk
+default_enabled: false
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 22cb7531a87..fd7ff91c6eb 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1393,6 +1393,31 @@ Input type: `AiActionInput`
| <a id="mutationaiactionerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationaiactionrequestid"></a>`requestId` | [`String`](#string) | ID of the request. |
+### `Mutation.aiAgentCreate`
+
+WARNING:
+**Introduced** in 16.8.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Input type: `AiAgentCreateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationaiagentcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationaiagentcreatename"></a>`name` | [`String!`](#string) | Name of the agent. |
+| <a id="mutationaiagentcreateprojectpath"></a>`projectPath` | [`ID!`](#id) | Project to which the agent belongs. |
+| <a id="mutationaiagentcreateprompt"></a>`prompt` | [`String!`](#string) | Prompt for the agent. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationaiagentcreateagent"></a>`agent` | [`AiAgent`](#aiagent) | Agent after mutation. |
+| <a id="mutationaiagentcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationaiagentcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+
### `Mutation.alertSetAssignees`
Input type: `AlertSetAssigneesInput`
@@ -14412,6 +14437,43 @@ Information about a connected Agent.
| <a id="agentmetadatapodnamespace"></a>`podNamespace` | [`String`](#string) | Namespace of the pod running the Agent. |
| <a id="agentmetadataversion"></a>`version` | [`String`](#string) | Agent version tag. |
+### `AiAgent`
+
+An AI agent.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="aiagent_links"></a>`_links` | [`AiAgentLinks!`](#aiagentlinks) | Map of links to perform actions on the agent. |
+| <a id="aiagentcreatedat"></a>`createdAt` | [`Time!`](#time) | Date of creation. |
+| <a id="aiagentid"></a>`id` | [`ID!`](#id) | ID of the agent. |
+| <a id="aiagentname"></a>`name` | [`String!`](#string) | Name of the agent. |
+| <a id="aiagentversions"></a>`versions` | [`[AiAgentVersion!]`](#aiagentversion) | Versions of the agent. |
+
+### `AiAgentLinks`
+
+Represents links to perform actions on the agent.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="aiagentlinksshowpath"></a>`showPath` | [`String`](#string) | Path to the details page of the agent. |
+
+### `AiAgentVersion`
+
+Version of an AI Agent.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="aiagentversioncreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp when the agent version was created. |
+| <a id="aiagentversionid"></a>`id` | [`ID!`](#id) | ID of the agent version. |
+| <a id="aiagentversionmodel"></a>`model` | [`String!`](#string) | Model of the agent. |
+| <a id="aiagentversionprompt"></a>`prompt` | [`String!`](#string) | Prompt of the agent. |
+
### `AiMessage`
AI features communication message.
diff --git a/doc/development/database_review.md b/doc/development/database_review.md
index 2d02d7e25de..2bb2a6fc267 100644
--- a/doc/development/database_review.md
+++ b/doc/development/database_review.md
@@ -153,7 +153,7 @@ Include in the MR description:
- Write the raw SQL in the MR description. Preferably formatted
nicely with [pgFormatter](https://sqlformat.darold.net) or
<https://paste.depesz.com> and using regular quotes
- (for example, `"projects"."id"`) and avoiding smart quotes (for example, `"projects"."id"`).
+ (for example, `"projects"."id"`) and avoiding smart quotes (for example, `“projects”.“id”`).
- In case of queries generated dynamically by using parameters, there should be one raw SQL query for each variation.
For example, a finder for issues that may take as a parameter an optional filter on projects,
diff --git a/lib/quality/seeders/issues.rb b/lib/quality/seeders/issues.rb
index 813ff0bf097..de7b275d43e 100644
--- a/lib/quality/seeders/issues.rb
+++ b/lib/quality/seeders/issues.rb
@@ -45,7 +45,7 @@ module Quality
created_at += 1.week
- break if created_at > Time.now
+ break if created_at.future?
end
created_issues_count
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d39d8a205a1..77fc5dc67e3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -19891,6 +19891,9 @@ msgstr ""
msgid "EscalationStatus|Triggered"
msgstr ""
+msgid "Escape"
+msgstr ""
+
msgid "Estimate"
msgstr ""
diff --git a/package.json b/package.json
index 7919ce67032..6c5c846fed5 100644
--- a/package.json
+++ b/package.json
@@ -235,11 +235,11 @@
"@gitlab/stylelint-config": "5.0.1",
"@graphql-eslint/eslint-plugin": "3.20.1",
"@originjs/vite-plugin-commonjs": "^1.0.3",
- "@rollup/plugin-graphql": "^2.0.3",
+ "@rollup/plugin-graphql": "^2.0.4",
"@testing-library/dom": "^7.16.2",
"@types/jest": "^28.1.3",
"@types/lodash": "^4.14.197",
- "@vitejs/plugin-vue2": "^1.1.2",
+ "@vitejs/plugin-vue2": "^2.3.1",
"@vue/compat": "^3.2.47",
"@vue/compiler-sfc": "^3.2.47",
"@vue/test-utils": "1.3.6",
@@ -283,7 +283,7 @@
"stylelint": "^15.10.2",
"swagger-cli": "^4.0.4",
"timezone-mock": "^1.0.8",
- "vite": "^5.0.0",
+ "vite": "^5.0.11",
"vite-plugin-ruby": "^5.0.0",
"vue-loader-vue3": "npm:vue-loader@17",
"vue-test-utils-compat": "0.0.14",
diff --git a/rubocop/cop/rails/avoid_time_comparison.rb b/rubocop/cop/rails/avoid_time_comparison.rb
new file mode 100644
index 00000000000..89aa0c2ebd0
--- /dev/null
+++ b/rubocop/cop/rails/avoid_time_comparison.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module Rails
+ # Checks for time comparison.
+ # For more information see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133520
+ #
+ # @example
+ # # bad
+ # datetime > Time.now
+ # Time.current < datetime
+ # datetime > Time.zone.now
+ #
+ # # good
+ # datetime.future?
+ # datetime.future?
+ # datetime.past?
+ class AvoidTimeComparison < RuboCop::Cop::Base
+ MSG = 'Avoid time comparison, use `.past?` or `.future?` instead.'
+ RESTRICT_ON_SEND = %i[< >].to_set.freeze
+
+ def_node_matcher :comparison?, <<~PATTERN
+ (send _ %RESTRICT_ON_SEND _)
+ PATTERN
+
+ def_node_matcher :time_now?, <<~PATTERN
+ {
+ (send
+ (const {nil? cbase} :Time) :now)
+ (send
+ (send
+ (const {nil? cbase} :Time) :zone) :now)
+ (send
+ (const {nil? cbase} :Time) :current)
+ }
+ PATTERN
+
+ def on_send(node)
+ return unless comparison?(node)
+
+ arg_check = node.arguments.find { |arg| time_now?(arg) }
+ receiver_check = time_now?(node.receiver)
+
+ add_offense(node) if arg_check || receiver_check
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/namespaces/descendants_spec.rb b/spec/models/namespaces/descendants_spec.rb
index 26d3619ea39..6c153c3307b 100644
--- a/spec/models/namespaces/descendants_spec.rb
+++ b/spec/models/namespaces/descendants_spec.rb
@@ -40,4 +40,29 @@ RSpec.describe Namespaces::Descendants, feature_category: :database do
)
end
end
+
+ describe '.expire_for' do
+ it 'sets the outdated_at column for the given namespace ids' do
+ freeze_time do
+ expire_time = Time.current
+
+ group1 = create(:group).tap do |g|
+ create(:namespace_descendants, namespace: g).reload.update!(outdated_at: nil)
+ end
+ group2 = create(:group, parent: group1).tap { |g| create(:namespace_descendants, namespace: g) }
+ group3 = create(:group, parent: group1)
+
+ group4 = create(:group).tap do |g|
+ create(:namespace_descendants, namespace: g).reload.update!(outdated_at: nil)
+ end
+
+ described_class.expire_for([group1.id, group2.id, group3.id])
+
+ expect(group1.namespace_descendants.outdated_at).to eq(expire_time)
+ expect(group2.namespace_descendants.outdated_at).to eq(expire_time)
+ expect(group3.namespace_descendants).to be_nil
+ expect(group4.namespace_descendants.outdated_at).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/namespaces/traversal/cached_spec.rb b/spec/models/namespaces/traversal/cached_spec.rb
new file mode 100644
index 00000000000..8263e28bb98
--- /dev/null
+++ b/spec/models/namespaces/traversal/cached_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespaces::Traversal::Cached, feature_category: :database do
+ let_it_be_with_refind(:old_parent) { create(:group) }
+ let_it_be_with_refind(:new_parent) { create(:group) }
+ let_it_be_with_refind(:group) { create(:group, parent: old_parent) }
+ let_it_be_with_refind(:subgroup) { create(:group, parent: group) }
+
+ context 'when the namespace_descendants_cache_expiration feature flag is off' do
+ let!(:cache) { create(:namespace_descendants, namespace: group) }
+
+ before do
+ stub_feature_flags(namespace_descendants_cache_expiration: false)
+ end
+
+ it 'does not invalidate the cache' do
+ expect { group.update!(parent: new_parent) }.not_to change { cache.reload.outdated_at }
+ end
+
+ context 'when the group is deleted' do
+ it 'invalidates the cache' do
+ expect { group.destroy! }.not_to change { cache.reload.outdated_at }
+ end
+ end
+ end
+
+ context 'when no cached records are present' do
+ it 'does nothing' do
+ group.parent = new_parent
+
+ expect { group.save! }.not_to change { Namespaces::Descendants.all.to_a }
+ end
+ end
+
+ context 'when the namespace record is UserNamespace' do
+ it 'does nothing' do
+ # we won't use the optimization for UserNamespace
+ namespace = create(:user_namespace)
+ cache = create(:namespace_descendants, namespace: namespace)
+
+ expect { namespace.destroy! }.not_to change { cache.reload.outdated_at }
+ end
+ end
+
+ context 'when cached record is present' do
+ let!(:cache) { create(:namespace_descendants, namespace: group) }
+
+ it 'invalidates the cache' do
+ expect { group.update!(parent: new_parent) }.to change { cache.reload.outdated_at }.from(nil)
+ end
+
+ it 'does not invalidate the cache of subgroups' do
+ subgroup_cache = create(:namespace_descendants, namespace: subgroup)
+
+ expect { group.update!(parent: new_parent) }.not_to change { subgroup_cache.reload.outdated_at }
+ end
+
+ context 'when a new subgroup is added' do
+ it 'invalidates the cache' do
+ expect { create(:group, parent: group) }.to change { cache.reload.outdated_at }
+ end
+ end
+
+ context 'when a new project is added' do
+ it 'invalidates the cache' do
+ expect { create(:project, group: group) }.to change { cache.reload.outdated_at }
+ end
+ end
+ end
+
+ context 'when parent group has cached record' do
+ it 'invalidates the parent cache' do
+ old_parent_cache = create(:namespace_descendants, namespace: old_parent)
+ new_parent_cache = create(:namespace_descendants, namespace: new_parent)
+
+ group.update!(parent: new_parent)
+
+ expect(old_parent_cache.reload.outdated_at).not_to be_nil
+ expect(new_parent_cache.reload.outdated_at).not_to be_nil
+ end
+ end
+
+ context 'when group is destroyed' do
+ it 'invalidates the cache' do
+ cache = create(:namespace_descendants, namespace: group)
+
+ expect { group.destroy! }.to change { cache.reload.outdated_at }.from(nil)
+ end
+
+ context 'when parent group has cached record' do
+ it 'invalidates the parent cache' do
+ old_parent_cache = create(:namespace_descendants, namespace: old_parent)
+ new_parent_cache = create(:namespace_descendants, namespace: new_parent)
+
+ group.destroy!
+
+ expect(old_parent_cache.reload.outdated_at).not_to be_nil
+ expect(new_parent_cache.reload.outdated_at).to be_nil # no change
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/rails/avoid_time_comparison_spec.rb b/spec/rubocop/cop/rails/avoid_time_comparison_spec.rb
new file mode 100644
index 00000000000..8ab430072b1
--- /dev/null
+++ b/spec/rubocop/cop/rails/avoid_time_comparison_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/rails/avoid_time_comparison'
+
+RSpec.describe RuboCop::Cop::Rails::AvoidTimeComparison, feature_category: :shared do
+ shared_examples 'using time comparison' do
+ let(:violation_string_length) { "datetime > #{time}".length }
+
+ it 'flags violation' do
+ expect_offense(<<~RUBY)
+ datetime > #{time}
+ #{'^' * violation_string_length} Avoid time comparison, use `.past?` or `.future?` instead.
+ RUBY
+
+ expect_offense(<<~RUBY)
+ datetime < #{time}
+ #{'^' * violation_string_length} Avoid time comparison, use `.past?` or `.future?` instead.
+ RUBY
+
+ expect_offense(<<~RUBY)
+ #{time} < datetime
+ #{'^' * violation_string_length} Avoid time comparison, use `.past?` or `.future?` instead.
+ RUBY
+ end
+ end
+
+ context 'when comparing with Time.now', :aggregate_failures do
+ let(:time) { 'Time.now' }
+
+ it_behaves_like 'using time comparison'
+ end
+
+ context 'when comparing with ::Time.now', :aggregate_failures do
+ let(:time) { '::Time.now' }
+
+ it_behaves_like 'using time comparison'
+ end
+
+ context 'when comparing with Time.zone.now', :aggregate_failures do
+ let(:time) { 'Time.zone.now' }
+
+ it_behaves_like 'using time comparison'
+ end
+
+ context 'when comparing with Time.current', :aggregate_failures do
+ let(:time) { 'Time.current' }
+
+ it_behaves_like 'using time comparison'
+ end
+
+ it 'does not flag assigning time methods to variables' do
+ expect_no_offenses(<<~RUBY)
+ datetime = Time.now
+ RUBY
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 26184387b8a..6bff776fc07 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1943,10 +1943,10 @@
dependencies:
type-fest "^2.0.0"
-"@rollup/plugin-graphql@^2.0.3":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@rollup/plugin-graphql/-/plugin-graphql-2.0.3.tgz#35fea077e225e2982ce8483dd6c381e8cca03aea"
- integrity sha512-IuuELo+0t29adRuLVg8izBFiUXFSFw8BmezespscynRfvfXSOV0S7g8RzQt75VzP6KHHVmNmlAgz+8qlkLur3w==
+"@rollup/plugin-graphql@^2.0.4":
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/@rollup/plugin-graphql/-/plugin-graphql-2.0.4.tgz#240aee16b4be10f3e9f879b6146af689cc10e07c"
+ integrity sha512-TfaqbbK71VHodCDCoRbPnv2+Tsnlvad2OsGEviURHFl+ZBUyf5wfXgXc9RqZ+xKxSl87Z3YbPhD0z6eWYjuByw==
dependencies:
"@rollup/pluginutils" "^5.0.1"
graphql-tag "^2.12.6"
@@ -2894,10 +2894,10 @@
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
-"@vitejs/plugin-vue2@^1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue2/-/plugin-vue2-1.1.2.tgz#891f0acc5a6a2b4886a74cb8d6359d42f19f968a"
- integrity sha512-y6OEA+2UdJ0xrEQHodq20v9r3SpS62IOHrgN92JPLvVpNkhcissu7yvD5PXMzMESyazj0XNWGsc8UQk8+mVrjQ==
+"@vitejs/plugin-vue2@^2.3.1":
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue2/-/plugin-vue2-2.3.1.tgz#53078d3d9d50d9863f1fbb1c1ef7791a5fcd4948"
+ integrity sha512-/ksaaz2SRLN11JQhLdEUhDzOn909WEk99q9t9w+N12GjQCljzv7GyvAbD/p20aBUjHkvpGOoQ+FCOkG+mjDF4A==
"@vue/apollo-components@^4.0.0-beta.4":
version "4.0.0-beta.4"
@@ -10132,7 +10132,7 @@ multicast-dns@^7.2.4:
dns-packet "^5.2.2"
thunky "^1.0.2"
-nanoid@^3.3.6:
+nanoid@^3.3.6, nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
@@ -10883,12 +10883,12 @@ postcss@^7.0.14, postcss@^7.0.36, postcss@^7.0.5, postcss@^7.0.6:
picocolors "^0.2.1"
source-map "^0.6.1"
-postcss@^8.1.10, postcss@^8.4.14, postcss@^8.4.25, postcss@^8.4.31:
- version "8.4.31"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
- integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
+postcss@^8.1.10, postcss@^8.4.14, postcss@^8.4.25, postcss@^8.4.32:
+ version "8.4.33"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742"
+ integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==
dependencies:
- nanoid "^3.3.6"
+ nanoid "^3.3.7"
picocolors "^1.0.0"
source-map-js "^1.0.2"
@@ -13471,13 +13471,13 @@ vite-plugin-ruby@^5.0.0:
debug "^4.3.4"
fast-glob "^3.3.2"
-vite@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.0.tgz#3bfb65acda2a97127e4fa240156664a1f234ce08"
- integrity sha512-ESJVM59mdyGpsiNAeHQOR/0fqNoOyWPYesFto8FFZugfmhdHx8Fzd8sF3Q/xkVhZsyOxHfdM7ieiVAorI9RjFw==
+vite@^5.0.11:
+ version "5.0.11"
+ resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.11.tgz#31562e41e004cb68e1d51f5d2c641ab313b289e4"
+ integrity sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==
dependencies:
esbuild "^0.19.3"
- postcss "^8.4.31"
+ postcss "^8.4.32"
rollup "^4.2.0"
optionalDependencies:
fsevents "~2.3.3"