diff --git a/lib/modules/manager/gradle/__fixtures__/2/libs.versions.toml b/lib/modules/manager/gradle/__fixtures__/2/libs.versions.toml
deleted file mode 100644
index 62f98ea855c26c27e708bc944e9542923093f3b1..0000000000000000000000000000000000000000
--- a/lib/modules/manager/gradle/__fixtures__/2/libs.versions.toml
+++ /dev/null
@@ -1,17 +0,0 @@
-[versions]
-kotlin = "1.5.21"
-retro_fit = "2.8.2"
-
-[libraries]
-okHttp = "com.squareup.okhttp3:okhttp:4.9.0"
-okio = { module = "com.squareup.okio:okio", version = "2.8.0" }
-picasso = { group = "com.squareup.picasso", name = "picasso", version = "2.5.1" }
-retrofit2-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retro_fit" }
-google-firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
-google-firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" }
-google-firebase-messaging = "com.google.firebase:firebase-messaging"
-
-[plugins]
-kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version = "1.5.21" }
-kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
-multiJvm = "org.danilopianini.multi-jvm-test-plugin:0.3.0"
diff --git a/lib/modules/manager/gradle/__fixtures__/3/libs.versions.toml b/lib/modules/manager/gradle/__fixtures__/3/libs.versions.toml
deleted file mode 100644
index 7188d1a6c6b443901fa7708e9154c60c197d7b52..0000000000000000000000000000000000000000
--- a/lib/modules/manager/gradle/__fixtures__/3/libs.versions.toml
+++ /dev/null
@@ -1,10 +0,0 @@
-[versions]
-# Releases: http://someWebsite.com/junit/1.4.9
-mocha-junit-reporter = "2.0.2"
-# JUnit 1.4.9 is awesome!
-junit = "1.4.9"
-
-
-[libraries]
-junit-legacy = { module = "junit:junit", version.ref = "junit" }
-mocha-junit = { module = "mocha-junit:mocha-junit", version.ref = "mocha.junit.reporter" }
diff --git a/lib/modules/manager/gradle/__fixtures__/1/libs.versions.toml b/lib/modules/manager/gradle/__fixtures__/libs.versions.toml
similarity index 100%
rename from lib/modules/manager/gradle/__fixtures__/1/libs.versions.toml
rename to lib/modules/manager/gradle/__fixtures__/libs.versions.toml
diff --git a/lib/modules/manager/gradle/extract.spec.ts b/lib/modules/manager/gradle/extract.spec.ts
index 7a0900ddfc2b3c8383e695d54eaaad061c2e3d90..bd11d3a32ac66ccc0d20dd48dfb7070f6a326239 100644
--- a/lib/modules/manager/gradle/extract.spec.ts
+++ b/lib/modules/manager/gradle/extract.spec.ts
@@ -1,4 +1,4 @@
-import { codeBlock, stripIndent } from 'common-tags';
+import { codeBlock } from 'common-tags';
 import { Fixtures } from '../../../../test/fixtures';
 import { fs, logger, partial } from '../../../../test/util';
 import type { ExtractConfig } from '../types';
@@ -499,7 +499,7 @@ describe('modules/manager/gradle/extract', () => {
   describe('version catalogs', () => {
     it('works with dependency catalogs', async () => {
       const fsMock = {
-        'gradle/libs.versions.toml': Fixtures.get('1/libs.versions.toml'),
+        'gradle/libs.versions.toml': Fixtures.get('libs.versions.toml'),
       };
       mockFs(fsMock);
 
@@ -614,118 +614,6 @@ describe('modules/manager/gradle/extract', () => {
       ]);
     });
 
-    it('supports versions declared as single string', async () => {
-      const fsMock = {
-        'gradle/libs.versions.toml': Fixtures.get('2/libs.versions.toml'),
-      };
-      mockFs(fsMock);
-
-      const res = await extractAllPackageFiles(
-        partial<ExtractConfig>(),
-        Object.keys(fsMock),
-      );
-
-      expect(res).toMatchObject([
-        {
-          packageFile: 'gradle/libs.versions.toml',
-          deps: [
-            {
-              depName: 'com.squareup.okhttp3:okhttp',
-              currentValue: '4.9.0',
-              managerData: {
-                fileReplacePosition: 100,
-                packageFile: 'gradle/libs.versions.toml',
-              },
-            },
-            {
-              depName: 'com.squareup.okio:okio',
-              currentValue: '2.8.0',
-              managerData: {
-                fileReplacePosition: 162,
-                packageFile: 'gradle/libs.versions.toml',
-              },
-            },
-            {
-              depName: 'com.squareup.picasso:picasso',
-              currentValue: '2.5.1',
-              managerData: {
-                fileReplacePosition: 244,
-                packageFile: 'gradle/libs.versions.toml',
-              },
-            },
-            {
-              depName: 'com.squareup.retrofit2:retrofit',
-              groupName: 'retro.fit',
-              currentValue: '2.8.2',
-              managerData: {
-                fileReplacePosition: 42,
-                packageFile: 'gradle/libs.versions.toml',
-              },
-            },
-            {
-              depName: 'google-firebase-analytics',
-              managerData: {
-                packageFile: 'gradle/libs.versions.toml',
-              },
-              skipReason: 'unspecified-version',
-            },
-            {
-              depName: 'google-firebase-crashlytics',
-              managerData: {
-                packageFile: 'gradle/libs.versions.toml',
-              },
-              skipReason: 'unspecified-version',
-            },
-            {
-              depName: 'google-firebase-messaging',
-              managerData: {
-                packageFile: 'gradle/libs.versions.toml',
-              },
-              skipReason: 'unspecified-version',
-            },
-            {
-              depName: 'org.jetbrains.kotlin.jvm',
-              depType: 'plugin',
-              currentValue: '1.5.21',
-              commitMessageTopic: 'plugin kotlinJvm',
-              packageName:
-                'org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin',
-              managerData: {
-                fileReplacePosition: 663,
-                packageFile: 'gradle/libs.versions.toml',
-              },
-              registryUrls: ['https://plugins.gradle.org/m2/'],
-            },
-            {
-              depName: 'org.jetbrains.kotlin.plugin.serialization',
-              depType: 'plugin',
-              currentValue: '1.5.21',
-              packageName:
-                'org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin',
-              managerData: {
-                fileReplacePosition: 21,
-                packageFile: 'gradle/libs.versions.toml',
-              },
-              registryUrls: ['https://plugins.gradle.org/m2/'],
-            },
-            {
-              depName: 'org.danilopianini.multi-jvm-test-plugin',
-              depType: 'plugin',
-              currentValue: '0.3.0',
-              commitMessageTopic: 'plugin multiJvm',
-              packageName:
-                'org.danilopianini.multi-jvm-test-plugin:org.danilopianini.multi-jvm-test-plugin.gradle.plugin',
-              managerData: {
-                fileReplacePosition: 824,
-                packageFile: 'gradle/libs.versions.toml',
-              },
-              registryUrls: ['https://plugins.gradle.org/m2/'],
-            },
-          ],
-        },
-      ]);
-    });
-
     it('ignores empty TOML file', async () => {
       const fsMock = {
         'gradle/libs.versions.toml': '',
@@ -739,97 +627,6 @@ describe('modules/manager/gradle/extract', () => {
         ),
       ).toBeNull();
     });
-
-    it('deletes commit message for plugins with version reference', async () => {
-      const fsMock = {
-        'gradle/libs.versions.toml': stripIndent`
-        [versions]
-        detekt = "1.18.1"
-
-        [plugins]
-        detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
-
-        [libraries]
-        detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
-      `,
-      };
-      mockFs(fsMock);
-
-      const res = await extractAllPackageFiles(
-        partial<ExtractConfig>(),
-        Object.keys(fsMock),
-      );
-      expect(res).toMatchObject([
-        {
-          packageFile: 'gradle/libs.versions.toml',
-          deps: [
-            {
-              depName: 'io.gitlab.arturbosch.detekt:detekt-formatting',
-              groupName: 'detekt',
-              currentValue: '1.18.1',
-              managerData: {
-                fileReplacePosition: 21,
-                packageFile: 'gradle/libs.versions.toml',
-              },
-              fileReplacePosition: 21,
-            },
-            {
-              depType: 'plugin',
-              depName: 'io.gitlab.arturbosch.detekt',
-              packageName:
-                'io.gitlab.arturbosch.detekt:io.gitlab.arturbosch.detekt.gradle.plugin',
-              registryUrls: ['https://plugins.gradle.org/m2/'],
-              currentValue: '1.18.1',
-              managerData: {
-                fileReplacePosition: 21,
-                packageFile: 'gradle/libs.versions.toml',
-              },
-              groupName: 'detekt',
-              fileReplacePosition: 21,
-            },
-          ],
-        },
-      ]);
-    });
-
-    it('changes the dependency version, not the comment version', async () => {
-      const fsMock = {
-        'gradle/libs.versions.toml': Fixtures.get('3/libs.versions.toml'),
-      };
-      mockFs(fsMock);
-
-      const res = await extractAllPackageFiles(
-        partial<ExtractConfig>(),
-        Object.keys(fsMock),
-      );
-      expect(res).toMatchObject([
-        {
-          packageFile: 'gradle/libs.versions.toml',
-          deps: [
-            {
-              depName: 'junit:junit',
-              groupName: 'junit',
-              currentValue: '1.4.9',
-              managerData: {
-                fileReplacePosition: 124,
-                packageFile: 'gradle/libs.versions.toml',
-              },
-              fileReplacePosition: 124,
-            },
-            {
-              depName: 'mocha-junit:mocha-junit',
-              groupName: 'mocha.junit.reporter',
-              currentValue: '2.0.2',
-              managerData: {
-                fileReplacePosition: 82,
-                packageFile: 'gradle/libs.versions.toml',
-              },
-              fileReplacePosition: 82,
-            },
-          ],
-        },
-      ]);
-    });
   });
 
   describe('apply from', () => {
@@ -996,7 +793,7 @@ describe('modules/manager/gradle/extract', () => {
     it('parses versions files', async () => {
       const fsMock = {
         'versions.props': `org.apache.lucene:* = 1.2.3`,
-        'versions.lock': stripIndent`
+        'versions.lock': codeBlock`
           # Run ./gradlew --write-locks to regenerate this file
           org.apache.lucene:lucene-core:1.2.3 (10 constraints: 95be0c15)
           org.apache.lucene:lucene-codecs:1.2.3 (5 constraints: 1231231)
@@ -1046,7 +843,7 @@ describe('modules/manager/gradle/extract', () => {
     it('plugin not used due to lockfile not a GCV lockfile', async () => {
       const fsMock = {
         'versions.props': `org.apache.lucene:* = 1.2.3`,
-        'versions.lock': stripIndent`
+        'versions.lock': codeBlock`
           This is NOT a lock file
         `,
       };
@@ -1072,117 +869,5 @@ describe('modules/manager/gradle/extract', () => {
       );
       expect(res).toBeNull();
     });
-
-    it('supports multiple levels of glob', async () => {
-      const fsMock = {
-        'versions.props': stripIndent`
-          org.apache.* = 4
-          org.apache.lucene:* = 3
-          org.apache.lucene:a.* = 2
-          org.apache.lucene:a.b = 1
-          org.apache.foo*:* = 5
-        `,
-        'versions.lock': stripIndent`
-          # Run ./gradlew --write-locks to regenerate this file
-          org.apache.solr:x.y:1 (10 constraints: 95be0c15)
-          org.apache.lucene:a.b:1 (10 constraints: 95be0c15)
-          org.apache.lucene:a.c:1 (10 constraints: 95be0c15)
-          org.apache.lucene:a.d:1 (10 constraints: 95be0c15)
-          org.apache.lucene:d:1 (10 constraints: 95be0c15)
-          org.apache.lucene:e.f:1 (10 constraints: 95be0c15)
-          org.apache.foo-bar:a:1 (10 constraints: 95be0c15)
-        `,
-      };
-      mockFs(fsMock);
-
-      const res = await extractAllPackageFiles(
-        partial<ExtractConfig>(),
-        Object.keys(fsMock),
-      );
-
-      // Each lock dep is only present once, with highest prio for exact prop match, then globs from longest to shortest
-      expect(res).toMatchObject([
-        {
-          packageFile: 'versions.lock',
-          deps: [],
-        },
-        {
-          packageFile: 'versions.props',
-          deps: [
-            {
-              managerData: {
-                packageFile: 'versions.props',
-                fileReplacePosition: 91,
-              },
-              depName: 'org.apache.lucene:a.b',
-              currentValue: '1',
-              lockedVersion: '1',
-              fileReplacePosition: 91,
-              depType: 'dependencies',
-            },
-            {
-              managerData: {
-                packageFile: 'versions.props',
-                fileReplacePosition: 65,
-              },
-              depName: 'org.apache.lucene:a.c',
-              currentValue: '2',
-              lockedVersion: '1',
-              groupName: 'org.apache.lucene:a.*',
-              fileReplacePosition: 65,
-              depType: 'dependencies',
-            },
-            {
-              managerData: {
-                packageFile: 'versions.props',
-                fileReplacePosition: 65,
-              },
-              depName: 'org.apache.lucene:a.d',
-              currentValue: '2',
-              lockedVersion: '1',
-              groupName: 'org.apache.lucene:a.*',
-              fileReplacePosition: 65,
-              depType: 'dependencies',
-            },
-            {
-              managerData: {
-                packageFile: 'versions.props',
-                fileReplacePosition: 39,
-              },
-              depName: 'org.apache.lucene:d',
-              currentValue: '3',
-              lockedVersion: '1',
-              groupName: 'org.apache.lucene:*',
-              fileReplacePosition: 39,
-              depType: 'dependencies',
-            },
-            {
-              managerData: {
-                packageFile: 'versions.props',
-                fileReplacePosition: 39,
-              },
-              depName: 'org.apache.lucene:e.f',
-              currentValue: '3',
-              lockedVersion: '1',
-              groupName: 'org.apache.lucene:*',
-              fileReplacePosition: 39,
-              depType: 'dependencies',
-            },
-            {
-              managerData: {
-                fileReplacePosition: 113,
-                packageFile: 'versions.props',
-              },
-              depName: 'org.apache.foo-bar:a',
-              currentValue: '5',
-              lockedVersion: '1',
-              groupName: 'org.apache.foo*:*',
-              fileReplacePosition: 113,
-              depType: 'dependencies',
-            },
-          ],
-        },
-      ]);
-    });
   });
 });
diff --git a/lib/modules/manager/gradle/extract/catalog.spec.ts b/lib/modules/manager/gradle/extract/catalog.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f1dc53281634a545be12c6bc27747a9d1fa46578
--- /dev/null
+++ b/lib/modules/manager/gradle/extract/catalog.spec.ts
@@ -0,0 +1,194 @@
+import { codeBlock } from 'common-tags';
+import { parseCatalog } from './catalog';
+
+describe('modules/manager/gradle/extract/catalog', () => {
+  it('supports versions declared as single string', () => {
+    const input = codeBlock`
+      [versions]
+      kotlin = "1.5.21"
+      retro_fit = "2.8.2"
+
+      [libraries]
+      okHttp = "com.squareup.okhttp3:okhttp:4.9.0"
+      okio = { module = "com.squareup.okio:okio", version = "2.8.0" }
+      picasso = { group = "com.squareup.picasso", name = "picasso", version = "2.5.1" }
+      retrofit2-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retro_fit" }
+      google-firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
+      google-firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" }
+      google-firebase-messaging = "com.google.firebase:firebase-messaging"
+
+      [plugins]
+      kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version = "1.5.21" }
+      kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+      multiJvm = "org.danilopianini.multi-jvm-test-plugin:0.3.0"
+    `;
+    const res = parseCatalog('gradle/libs.versions.toml', input);
+    expect(res).toStrictEqual([
+      {
+        depName: 'com.squareup.okhttp3:okhttp',
+        currentValue: '4.9.0',
+        managerData: {
+          fileReplacePosition: 100,
+          packageFile: 'gradle/libs.versions.toml',
+        },
+      },
+      {
+        depName: 'com.squareup.okio:okio',
+        currentValue: '2.8.0',
+        managerData: {
+          fileReplacePosition: 162,
+          packageFile: 'gradle/libs.versions.toml',
+        },
+      },
+      {
+        depName: 'com.squareup.picasso:picasso',
+        currentValue: '2.5.1',
+        managerData: {
+          fileReplacePosition: 244,
+          packageFile: 'gradle/libs.versions.toml',
+        },
+      },
+      {
+        depName: 'com.squareup.retrofit2:retrofit',
+        groupName: 'retro.fit',
+        currentValue: '2.8.2',
+        managerData: {
+          fileReplacePosition: 42,
+          packageFile: 'gradle/libs.versions.toml',
+        },
+      },
+      {
+        depName: 'google-firebase-analytics',
+        managerData: {
+          packageFile: 'gradle/libs.versions.toml',
+        },
+        skipReason: 'unspecified-version',
+      },
+      {
+        depName: 'google-firebase-crashlytics',
+        managerData: {
+          packageFile: 'gradle/libs.versions.toml',
+        },
+        skipReason: 'unspecified-version',
+      },
+      {
+        depName: 'google-firebase-messaging',
+        managerData: {
+          packageFile: 'gradle/libs.versions.toml',
+        },
+        skipReason: 'unspecified-version',
+      },
+      {
+        depName: 'org.jetbrains.kotlin.jvm',
+        depType: 'plugin',
+        currentValue: '1.5.21',
+        commitMessageTopic: 'plugin kotlinJvm',
+        packageName:
+          'org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin',
+        managerData: {
+          fileReplacePosition: 663,
+          packageFile: 'gradle/libs.versions.toml',
+        },
+      },
+      {
+        depName: 'org.jetbrains.kotlin.plugin.serialization',
+        depType: 'plugin',
+        currentValue: '1.5.21',
+        groupName: 'kotlin',
+        packageName:
+          'org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin',
+        managerData: {
+          fileReplacePosition: 21,
+          packageFile: 'gradle/libs.versions.toml',
+        },
+      },
+      {
+        depName: 'org.danilopianini.multi-jvm-test-plugin',
+        depType: 'plugin',
+        currentValue: '0.3.0',
+        commitMessageTopic: 'plugin multiJvm',
+        packageName:
+          'org.danilopianini.multi-jvm-test-plugin:org.danilopianini.multi-jvm-test-plugin.gradle.plugin',
+        managerData: {
+          fileReplacePosition: 824,
+          packageFile: 'gradle/libs.versions.toml',
+        },
+      },
+    ]);
+  });
+
+  it('deletes commit message for plugins with version reference', () => {
+    const input = codeBlock`
+      [versions]
+      detekt = "1.18.1"
+
+      [plugins]
+      detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
+
+      [libraries]
+      detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
+    `;
+    const res = parseCatalog('gradle/libs.versions.toml', input);
+
+    expect(res).toStrictEqual([
+      {
+        depName: 'io.gitlab.arturbosch.detekt:detekt-formatting',
+        groupName: 'detekt',
+        currentValue: '1.18.1',
+        managerData: {
+          fileReplacePosition: 21,
+          packageFile: 'gradle/libs.versions.toml',
+        },
+      },
+      {
+        depType: 'plugin',
+        depName: 'io.gitlab.arturbosch.detekt',
+        packageName:
+          'io.gitlab.arturbosch.detekt:io.gitlab.arturbosch.detekt.gradle.plugin',
+        currentValue: '1.18.1',
+        managerData: {
+          fileReplacePosition: 21,
+          packageFile: 'gradle/libs.versions.toml',
+        },
+        groupName: 'detekt',
+      },
+    ]);
+  });
+
+  it('changes the dependency version, not the comment version', () => {
+    const input = codeBlock`
+      [versions]
+      # Releases: http://someWebsite.com/junit/1.4.9
+      mocha-junit-reporter = "2.0.2"
+      # JUnit 1.4.9 is awesome!
+      junit = "1.4.9"
+
+
+      [libraries]
+      junit-legacy = { module = "junit:junit", version.ref = "junit" }
+      mocha-junit = { module = "mocha-junit:mocha-junit", version.ref = "mocha.junit.reporter" }
+    `;
+    const res = parseCatalog('gradle/libs.versions.toml', input);
+
+    expect(res).toStrictEqual([
+      {
+        depName: 'junit:junit',
+        groupName: 'junit',
+        currentValue: '1.4.9',
+        managerData: {
+          fileReplacePosition: 124,
+          packageFile: 'gradle/libs.versions.toml',
+        },
+      },
+      {
+        depName: 'mocha-junit:mocha-junit',
+        groupName: 'mocha.junit.reporter',
+        currentValue: '2.0.2',
+        managerData: {
+          fileReplacePosition: 82,
+          packageFile: 'gradle/libs.versions.toml',
+        },
+      },
+    ]);
+  });
+});
diff --git a/lib/modules/manager/gradle/extract/consistent-versions-plugin.spec.ts b/lib/modules/manager/gradle/extract/consistent-versions-plugin.spec.ts
index 5ebaef86501b142d7dbb60b22b8a64c32e2ac277..fdde721e725453eeb5c7129895f20fffbd9faa89 100644
--- a/lib/modules/manager/gradle/extract/consistent-versions-plugin.spec.ts
+++ b/lib/modules/manager/gradle/extract/consistent-versions-plugin.spec.ts
@@ -1,16 +1,17 @@
-import { stripIndent } from 'common-tags';
+import { codeBlock } from 'common-tags';
 import {
+  parseGcv,
   parseLockFile,
   parsePropsFile,
   usesGcv,
 } from './consistent-versions-plugin';
 
 describe('modules/manager/gradle/extract/consistent-versions-plugin', () => {
-  it('gradle-consistent-versions plugin works for sub folders', () => {
+  it('works for sub folders', () => {
     const fsMock = {
       'mysub/build.gradle.kts': `(this file contains) 'com.palantir.consistent-versions'`,
       'mysub/versions.props': `org.apache.lucene:* = 1.2.3`,
-      'mysub/versions.lock': stripIndent`
+      'mysub/versions.lock': codeBlock`
         # Run ./gradlew --write-locks to regenerate this file
         org.apache.lucene:lucene-core:1.2.3`,
       'othersub/build.gradle.kts': `nothing here`,
@@ -24,7 +25,7 @@ describe('modules/manager/gradle/extract/consistent-versions-plugin', () => {
     const fsMock = {
       'build.gradle.kts': `(this file contains) 'com.palantir.consistent-versions'`,
       'versions.props': `org.apache.lucene:* = 1.2.3`,
-      'versions.lock': stripIndent`
+      'versions.lock': codeBlock`
         # Run ./gradlew writeVersionsLock to regenerate this file
         org.apache.lucene:lucene-core:1.2.3`,
     };
@@ -36,7 +37,7 @@ describe('modules/manager/gradle/extract/consistent-versions-plugin', () => {
     const fsMock = {
       'build.gradle.kts': `(this file contains) 'com.palantir.consistent-versions'`,
       'versions.props': `org.apache.lucene:* = 1.2.3`,
-      'versions.lock': stripIndent`
+      'versions.lock': codeBlock`
         # Run ./gradlew writeVersionsLocks to regenerate this file
         org.apache.lucene:lucene-core:1.2.3`,
     };
@@ -44,7 +45,7 @@ describe('modules/manager/gradle/extract/consistent-versions-plugin', () => {
     expect(usesGcv('versions.props', fsMock)).toBeTrue();
   });
 
-  it('gradle-consistent-versions plugin correct position for CRLF and LF', () => {
+  it('correct position for CRLF and LF', () => {
     const crlfProps = parsePropsFile(`a.b:c.d=1\r\na.b:c.e=2`);
     expect(crlfProps).toBeArrayOfSize(2);
     expect(crlfProps[0].has('a.b:c.e')).toBeTrue();
@@ -56,8 +57,8 @@ describe('modules/manager/gradle/extract/consistent-versions-plugin', () => {
     expect(lfProps[0].get('a.b:c.e')).toMatchObject({ filePos: 18 });
   });
 
-  it('gradle-consistent-versions plugin test bogus input lines', () => {
-    const parsedProps = parsePropsFile(stripIndent`
+  it('test bogus input lines', () => {
+    const parsedProps = parsePropsFile(codeBlock`
       # comment:foo.bar = 1
       123.foo:bar = 2
       this has:spaces = 3
@@ -71,7 +72,7 @@ describe('modules/manager/gradle/extract/consistent-versions-plugin', () => {
     expect(parsedProps[0]).toMatchObject({ size: 1 }); // no 7 is valid exact dep
     expect(parsedProps[1]).toMatchObject({ size: 1 }); // no 8 is valid glob dep
 
-    const parsedLock = parseLockFile(stripIndent`
+    const parsedLock = parseLockFile(codeBlock`
       # comment:foo.bar:1 (10 constraints: 95be0c15)
       123.foo:bar:2 (10 constraints: 95be0c15)
       this has:spaces:3 (10 constraints: 95be0c15)
@@ -92,4 +93,96 @@ describe('modules/manager/gradle/extract/consistent-versions-plugin', () => {
       depType: 'test',
     });
   });
+
+  it('supports multiple levels of glob', () => {
+    const fsMock = {
+      'versions.props': codeBlock`
+          org.apache.* = 4
+          org.apache.lucene:* = 3
+          org.apache.lucene:a.* = 2
+          org.apache.lucene:a.b = 1
+          org.apache.foo*:* = 5
+        `,
+      'versions.lock': codeBlock`
+          # Run ./gradlew --write-locks to regenerate this file
+          org.apache.solr:x.y:1 (10 constraints: 95be0c15)
+          org.apache.lucene:a.b:1 (10 constraints: 95be0c15)
+          org.apache.lucene:a.c:1 (10 constraints: 95be0c15)
+          org.apache.lucene:a.d:1 (10 constraints: 95be0c15)
+          org.apache.lucene:d:1 (10 constraints: 95be0c15)
+          org.apache.lucene:e.f:1 (10 constraints: 95be0c15)
+          org.apache.foo-bar:a:1 (10 constraints: 95be0c15)
+        `,
+    };
+    const res = parseGcv('versions.props', fsMock);
+
+    // Each lock dep is only present once, with highest prio for exact prop match, then globs from longest to shortest
+    expect(res).toStrictEqual([
+      {
+        managerData: {
+          packageFile: 'versions.props',
+          fileReplacePosition: 91,
+        },
+        depName: 'org.apache.lucene:a.b',
+        currentValue: '1',
+        lockedVersion: '1',
+        depType: 'dependencies',
+      },
+      {
+        managerData: {
+          packageFile: 'versions.props',
+          fileReplacePosition: 65,
+        },
+        depName: 'org.apache.lucene:a.c',
+        currentValue: '2',
+        lockedVersion: '1',
+        groupName: 'org.apache.lucene:a.*',
+        depType: 'dependencies',
+      },
+      {
+        managerData: {
+          packageFile: 'versions.props',
+          fileReplacePosition: 65,
+        },
+        depName: 'org.apache.lucene:a.d',
+        currentValue: '2',
+        lockedVersion: '1',
+        groupName: 'org.apache.lucene:a.*',
+        depType: 'dependencies',
+      },
+      {
+        managerData: {
+          packageFile: 'versions.props',
+          fileReplacePosition: 39,
+        },
+        depName: 'org.apache.lucene:d',
+        currentValue: '3',
+        lockedVersion: '1',
+        groupName: 'org.apache.lucene:*',
+        depType: 'dependencies',
+      },
+      {
+        managerData: {
+          packageFile: 'versions.props',
+          fileReplacePosition: 39,
+        },
+        depName: 'org.apache.lucene:e.f',
+        currentValue: '3',
+        lockedVersion: '1',
+        groupName: 'org.apache.lucene:*',
+        depType: 'dependencies',
+      },
+      {
+        managerData: {
+          fileReplacePosition: 113,
+          packageFile: 'versions.props',
+        },
+        depName: 'org.apache.foo-bar:a',
+        currentValue: '5',
+        lockedVersion: '1',
+        groupName: 'org.apache.foo*:*',
+        depType: 'dependencies',
+      },
+    ]);
+  });
 });
diff --git a/lib/modules/manager/gradle/utils.spec.ts b/lib/modules/manager/gradle/utils.spec.ts
index 1de059b86c62b95004db6a1551d62bbb6f5e65fb..d8879d0ebf2bd34c1d8805996922c1c832dd08c8 100644
--- a/lib/modules/manager/gradle/utils.spec.ts
+++ b/lib/modules/manager/gradle/utils.spec.ts
@@ -1,7 +1,13 @@
-import type { VariableRegistry } from './types';
+import type { PackageVariables, VariableRegistry } from './types';
 import {
   getVars,
   isDependencyString,
+  isGradleBuildFile,
+  isGradleScriptFile,
+  isGradleVersionsFile,
+  isKotlinSourceFile,
+  isPropsFile,
+  isTOMLFile,
   parseDependencyString,
   reorderFiles,
   toAbsolutePath,
@@ -10,66 +16,83 @@ import {
 } from './utils';
 
 describe('modules/manager/gradle/utils', () => {
-  it('versionLikeSubstring', () => {
-    [
-      '1.2.3',
-      '[1.0,2.0]',
-      '(,2.0[',
-      '2.1.1.RELEASE',
-      '1.0.+',
-      '2022-05-10_55',
-    ].forEach((input) => {
-      expect(versionLikeSubstring(input)).toEqual(input);
-      expect(versionLikeSubstring(`${input}'`)).toEqual(input);
-      expect(versionLikeSubstring(`${input}"`)).toEqual(input);
-      expect(versionLikeSubstring(`${input}\n`)).toEqual(input);
-      expect(versionLikeSubstring(`${input}  `)).toEqual(input);
-      expect(versionLikeSubstring(`${input}$`)).toEqual(input);
+  describe('versionLikeSubstring', () => {
+    it('extracts the actual version', () => {
+      const inputs = [
+        '1.2.3',
+        '[1.0,2.0]',
+        '(,2.0[',
+        '2.1.1.RELEASE',
+        '1.0.+',
+        '2022-05-10_55',
+      ];
+      const suffixes = ['', "'", '"', '\n', '  ', '$'];
+
+      for (const input of inputs) {
+        for (const suffix of suffixes) {
+          expect(versionLikeSubstring(`${input}${suffix}`)).toEqual(input);
+        }
+      }
     });
-    expect(versionLikeSubstring('')).toBeNull();
-    expect(versionLikeSubstring(undefined)).toBeNull();
-    expect(versionLikeSubstring(null)).toBeNull();
-    expect(versionLikeSubstring('foobar')).toBeNull();
-    expect(versionLikeSubstring('latest')).toBeNull();
-  });
 
-  it('isDependencyString', () => {
-    expect(isDependencyString('foo:bar:1.2.3')).toBeTrue();
-    expect(isDependencyString('foo.foo:bar.bar:1.2.3')).toBeTrue();
-    expect(isDependencyString('foo:bar:baz:qux')).toBeFalse();
-    expect(isDependencyString('foo.bar:baz:1.2.3')).toBeTrue();
-    expect(isDependencyString('foo.bar:baz:1.2.3:linux-cpu-x86_64')).toBeTrue();
-    expect(isDependencyString('foo.bar:baz:1.2.+')).toBeTrue();
-    expect(isDependencyString('foo:bar:baz:qux:quux')).toBeFalse();
-    expect(isDependencyString("foo:bar:1.2.3'")).toBeFalse();
-    expect(isDependencyString('foo:bar:1.2.3"')).toBeFalse();
-    expect(isDependencyString('-Xep:ParameterName:OFF')).toBeFalse();
-    expect(isDependencyString('foo$bar:baz:1.2.+')).toBeFalse();
-    expect(isDependencyString('scm:git:https://some.git')).toBeFalse();
+    it('returns null for invalid inputs', () => {
+      const inputs = ['', undefined, null, 'foobar', 'latest'];
+      for (const input of inputs) {
+        expect(versionLikeSubstring(input)).toBeNull();
+      }
+    });
   });
 
-  it('parseDependencyString', () => {
-    expect(parseDependencyString('foo:bar:1.2.3')).toMatchObject({
-      depName: 'foo:bar',
-      currentValue: '1.2.3',
+  describe('isDependencyString', () => {
+    it.each`
+      input                                    | output
+      ${'foo:bar:1.2.3'}                       | ${true}
+      ${'foo.foo:bar.bar:1.2.3'}               | ${true}
+      ${'foo.bar:baz:1.2.3'}                   | ${true}
+      ${'foo.bar:baz:1.2.3:linux-cpu-x86_64'}  | ${true}
+      ${'foo:bar:1.2.3@zip'}                   | ${true}
+      ${'foo:bar:x86@x86'}                     | ${true}
+      ${'foo.bar:baz:1.2.+'}                   | ${true}
+      ${'foo:bar:baz:qux'}                     | ${false}
+      ${'foo:bar:baz:qux:quux'}                | ${false}
+      ${"foo:bar:1.2.3'"}                      | ${false}
+      ${'foo:bar:1.2.3"'}                      | ${false}
+      ${'-Xep:ParameterName:OFF'}              | ${false}
+      ${'foo$bar:baz:1.2.+'}                   | ${false}
+      ${'scm:git:https://some.git'}            | ${false}
+      ${'foo.bar:baz:1.2.3:linux-cpu$-x86_64'} | ${false}
+      ${'foo:bar:1.2.3@zip@foo'}               | ${false}
+    `('$input', ({ input, output }) => {
+      expect(isDependencyString(input)).toBe(output);
     });
-    expect(parseDependencyString('foo.foo:bar.bar:1.2.3')).toMatchObject({
-      depName: 'foo.foo:bar.bar',
-      currentValue: '1.2.3',
-    });
-    expect(parseDependencyString('foo.bar:baz:1.2.3')).toMatchObject({
-      depName: 'foo.bar:baz',
-      currentValue: '1.2.3',
-    });
-    expect(parseDependencyString('foo:bar:1.2.+')).toMatchObject({
-      depName: 'foo:bar',
-      currentValue: '1.2.+',
+  });
+
+  describe('parseDependencyString', () => {
+    it.each`
+      input                       | output
+      ${'foo:bar:1.2.3'}          | ${{ depName: 'foo:bar', currentValue: '1.2.3' }}
+      ${'foo.foo:bar.bar:1.2.3'}  | ${{ depName: 'foo.foo:bar.bar', currentValue: '1.2.3' }}
+      ${'foo.bar:baz:1.2.3'}      | ${{ depName: 'foo.bar:baz', currentValue: '1.2.3' }}
+      ${'foo:bar:1.2.+'}          | ${{ depName: 'foo:bar', currentValue: '1.2.+' }}
+      ${'foo:bar:1.2.3@zip'}      | ${{ depName: 'foo:bar', currentValue: '1.2.3', dataType: 'zip' }}
+      ${'foo:bar:baz:qux'}        | ${null}
+      ${'foo:bar:baz:qux:quux'}   | ${null}
+      ${"foo:bar:1.2.3'"}         | ${null}
+      ${'foo:bar:1.2.3"'}         | ${null}
+      ${'-Xep:ParameterName:OFF'} | ${null}
+    `('$input', ({ input, output }) => {
+      expect(parseDependencyString(input)).toEqual(output);
     });
-    expect(parseDependencyString('foo:bar:baz:qux')).toBeNull();
-    expect(parseDependencyString('foo:bar:baz:qux:quux')).toBeNull();
-    expect(parseDependencyString("foo:bar:1.2.3'")).toBeNull();
-    expect(parseDependencyString('foo:bar:1.2.3"')).toBeNull();
-    expect(parseDependencyString('-Xep:ParameterName:OFF')).toBeNull();
+  });
+
+  it('filetype checks', () => {
+    expect(isGradleScriptFile('/a/Somefile.gradle.kts')).toBeTrue();
+    expect(isGradleScriptFile('/a/Somefile.gradle')).toBeTrue();
+    expect(isGradleVersionsFile('/a/versions.gradle.kts')).toBeTrue();
+    expect(isGradleBuildFile('/a/build.gradle')).toBeTrue();
+    expect(isPropsFile('/a/gradle.properties')).toBeTrue();
+    expect(isKotlinSourceFile('/a/Somefile.kt')).toBeTrue();
+    expect(isTOMLFile('/a/Somefile.toml')).toBeTrue();
   });
 
   it('reorderFiles', () => {
@@ -175,20 +198,33 @@ describe('modules/manager/gradle/utils', () => {
     });
   });
 
-  it('updateVars', () => {
-    const registry: VariableRegistry = {
-      [toAbsolutePath('/foo/bar/baz')]: {
+  describe('updateVars', () => {
+    it('empty registry', () => {
+      const registry: VariableRegistry = {};
+      const newVars: PackageVariables = {
+        qux: { key: 'qux', value: 'qux' },
+      };
+      updateVars(registry, '/foo/bar/baz', newVars);
+      expect(registry).toStrictEqual({ '/foo/bar/baz': newVars });
+    });
+
+    it('updates the registry', () => {
+      const registry: VariableRegistry = {
+        [toAbsolutePath('/foo/bar/baz')]: {
+          bar: { key: 'bar', value: 'bar' },
+          baz: { key: 'baz', value: 'baz' },
+        },
+      };
+
+      updateVars(registry, '/foo/bar/baz', {
+        qux: { key: 'qux', value: 'qux' },
+      });
+      const res = getVars(registry, '/foo/bar/baz/build.gradle');
+      expect(res).toStrictEqual({
         bar: { key: 'bar', value: 'bar' },
         baz: { key: 'baz', value: 'baz' },
-      },
-    };
-
-    updateVars(registry, '/foo/bar/baz', { qux: { key: 'qux', value: 'qux' } });
-    const res = getVars(registry, '/foo/bar/baz/build.gradle');
-    expect(res).toStrictEqual({
-      bar: { key: 'bar', value: 'bar' },
-      baz: { key: 'baz', value: 'baz' },
-      qux: { key: 'qux', value: 'qux' },
+        qux: { key: 'qux', value: 'qux' },
+      });
     });
   });
 });