Skip to content

Commit

Permalink
feat(gradle): add support for gradle repository content descriptors (#…
Browse files Browse the repository at this point in the history
…33692)

Co-authored-by: Michael Kriese <[email protected]>
  • Loading branch information
Churro and viceice authored Feb 4, 2025
1 parent 4cfc794 commit aae045e
Show file tree
Hide file tree
Showing 8 changed files with 619 additions and 16 deletions.
212 changes: 211 additions & 1 deletion lib/modules/manager/gradle/extract.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { codeBlock } from 'common-tags';
import { Fixtures } from '../../../../test/fixtures';
import { fs, logger, partial } from '../../../../test/util';
import type { ExtractConfig } from '../types';
import type { ExtractConfig, PackageDependency } from '../types';
import { matchesContentDescriptor } from './extract';
import * as parser from './parser';
import { extractAllPackageFiles } from '.';

Expand Down Expand Up @@ -494,6 +495,215 @@ describe('modules/manager/gradle/extract', () => {
},
]);
});

describe('content descriptors', () => {
describe('simple descriptor matches', () => {
it.each`
input | output | descriptor
${'foo:bar:1.2.3'} | ${true} | ${undefined}
${'foo:bar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo' }]}
${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'exclude', matcher: 'simple', groupId: 'foo' }]}
${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'simple', groupId: 'bar' }]}
${'foo:bar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'bar' }]}
${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'exclude', matcher: 'simple', groupId: 'foo', artifactId: 'bar' }]}
${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'baz' }]}
${'foo:bar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'bar', version: '1.2.3' }]}
${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'exclude', matcher: 'simple', groupId: 'foo', artifactId: 'bar', version: '1.2.3' }]}
${'foo:bar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'bar', version: '1.2.+' }]}
${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'baz', version: '4.5.6' }]}
${'foo:bar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'subgroup', groupId: 'foo' }]}
${'foo.bar.baz:qux:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'subgroup', groupId: 'foo.bar.baz' }]}
${'foo.bar.baz:qux:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'subgroup', groupId: 'foo.bar' }]}
${'foo.bar.baz:qux:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'subgroup', groupId: 'foo.barbaz' }]}
${'foobarbaz:qux:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'regex', groupId: '.*bar.*' }]}
${'foobarbaz:qux:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'regex', groupId: '.*bar.*', artifactId: 'qux' }]}
${'foobar:foobar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'regex', groupId: '.*bar.*', artifactId: 'foo.*' }]}
${'foobar:foobar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'regex', groupId: 'foobar', artifactId: '^bar' }]}
${'foobar:foobar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'regex', groupId: 'foobar', artifactId: '^foo.*', version: '1\\.*' }]}
${'foobar:foobar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'regex', groupId: 'foobar', artifactId: '^foo', version: '3.+' }]}
${'foobar:foobar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'regex', groupId: 'foobar', artifactId: 'qux', version: '1\\.*' }]}
`('$input | $output', ({ input, output, descriptor }) => {
const [groupId, artifactId, currentValue] = input.split(':');
const dep: PackageDependency = {
depName: `${groupId}:${artifactId}`,
currentValue,
};

expect(matchesContentDescriptor(dep, descriptor)).toBe(output);
});
});

describe('multiple descriptors', () => {
const dep: PackageDependency = {
depName: `foo:bar`,
currentValue: '1.2.3',
};

it('if both includes and excludes exist, dep must match include and not match exclude', () => {
expect(
matchesContentDescriptor(dep, [
{ mode: 'include', matcher: 'simple', groupId: 'foo' },
{
mode: 'exclude',
matcher: 'simple',
groupId: 'foo',
artifactId: 'baz',
},
]),
).toBe(true);

expect(
matchesContentDescriptor(dep, [
{ mode: 'include', matcher: 'simple', groupId: 'foo' },
{
mode: 'exclude',
matcher: 'simple',
groupId: 'foo',
artifactId: 'bar',
},
]),
).toBe(false);
});

it('if only includes exist, dep must match at least one include', () => {
expect(
matchesContentDescriptor(dep, [
{ mode: 'include', matcher: 'simple', groupId: 'some' },
{ mode: 'include', matcher: 'simple', groupId: 'foo' },
{ mode: 'include', matcher: 'simple', groupId: 'bar' },
]),
).toBe(true);

expect(
matchesContentDescriptor(dep, [
{ mode: 'include', matcher: 'simple', groupId: 'some' },
{ mode: 'include', matcher: 'simple', groupId: 'other' },
{ mode: 'include', matcher: 'simple', groupId: 'bar' },
]),
).toBe(false);
});

it('if only excludes exist, dep must match not match any exclude', () => {
expect(
matchesContentDescriptor(dep, [
{ mode: 'exclude', matcher: 'simple', groupId: 'some' },
{ mode: 'exclude', matcher: 'simple', groupId: 'foo' },
{ mode: 'exclude', matcher: 'simple', groupId: 'bar' },
]),
).toBe(false);

expect(
matchesContentDescriptor(dep, [
{ mode: 'exclude', matcher: 'simple', groupId: 'some' },
{ mode: 'exclude', matcher: 'simple', groupId: 'other' },
{ mode: 'exclude', matcher: 'simple', groupId: 'bar' },
]),
).toBe(true);
});
});

it('extracts content descriptors', async () => {
const fsMock = {
'build.gradle': codeBlock`
pluginManagement {
repositories {
maven {
url = "https://foo.bar/baz"
content {
includeModule("com.diffplug.spotless", "com.diffplug.spotless.gradle.plugin")
}
}
}
}
repositories {
mavenCentral()
google {
content {
includeGroupAndSubgroups("foo.bar")
includeModuleByRegex("com\\\\.(google|android).*", "protobuf.*")
includeGroupByRegex("(?!(unsupported|pattern).*)")
includeGroupByRegex "org\\\\.jetbrains\\\\.kotlin.*"
excludeModule("foo.bar.group", "simple.module")
}
}
maven {
name = "some"
url = "https://foo.bar/\${name}"
content {
includeModule("foo.bar.group", "simple.module")
includeVersion("com.google.protobuf", "protobuf-java", "2.17.+")
}
}
}
plugins {
id("com.diffplug.spotless") version "6.10.0"
}
dependencies {
implementation "com.google.protobuf:protobuf-java:2.17.1"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.21"
implementation "foo.bar:protobuf-java:2.17.0"
implementation "foo.bar.group:simple.module:2.17.0"
}
`,
};
mockFs(fsMock);

const res = await extractAllPackageFiles(
partial<ExtractConfig>(),
Object.keys(fsMock),
);

expect(res).toMatchObject([
{
deps: [
{
depName: 'com.diffplug.spotless',
currentValue: '6.10.0',
depType: 'plugin',
packageName:
'com.diffplug.spotless:com.diffplug.spotless.gradle.plugin',
registryUrls: ['https://foo.bar/baz'],
},
{
depName: 'com.google.protobuf:protobuf-java',
currentValue: '2.17.1',
registryUrls: [
'https://repo.maven.apache.org/maven2',
'https://dl.google.com/android/maven2/',
'https://foo.bar/some',
],
},
{
depName: 'org.jetbrains.kotlin:kotlin-stdlib-jdk8',
currentValue: '1.4.21',
registryUrls: [
'https://repo.maven.apache.org/maven2',
'https://dl.google.com/android/maven2/',
],
},
{
depName: 'foo.bar:protobuf-java',
currentValue: '2.17.0',
registryUrls: [
'https://repo.maven.apache.org/maven2',
'https://dl.google.com/android/maven2/',
],
},
{
depName: 'foo.bar.group:simple.module',
currentValue: '2.17.0',
registryUrls: [
'https://repo.maven.apache.org/maven2',
'https://foo.bar/some',
],
},
],
},
]);
});
});
});

describe('version catalogs', () => {
Expand Down
88 changes: 88 additions & 0 deletions lib/modules/manager/gradle/extract.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import upath from 'upath';
import { logger } from '../../../logger';
import { coerceArray } from '../../../util/array';
import { getLocalFiles } from '../../../util/fs';
import { regEx } from '../../../util/regex';
import { MavenDatasource } from '../../datasource/maven';
import gradleVersioning from '../../versioning/gradle';
import type { ExtractConfig, PackageDependency, PackageFile } from '../types';
import { parseCatalog } from './extract/catalog';
import {
Expand All @@ -12,6 +15,7 @@ import {
import { parseGradle, parseKotlinSource, parseProps } from './parser';
import { REGISTRY_URLS } from './parser/common';
import type {
ContentDescriptorSpec,
GradleManagerData,
PackageRegistry,
VariableRegistry,
Expand Down Expand Up @@ -44,6 +48,89 @@ function updatePackageRegistries(
}
}

export function matchesContentDescriptor(
dep: PackageDependency<GradleManagerData>,
contentDescriptors?: ContentDescriptorSpec[],
): boolean {
const [groupId, artifactId] = (dep.packageName ?? dep.depName!).split(':');
let hasIncludes = false;
let hasExcludes = false;
let matchesInclude = false;
let matchesExclude = false;

for (const content of coerceArray(contentDescriptors)) {
const {
mode,
matcher,
groupId: contentGroupId,
artifactId: contentArtifactId,
version: contentVersion,
} = content;

// group matching
let groupMatch = false;
if (matcher === 'regex') {
groupMatch = regEx(contentGroupId).test(groupId);
} else if (matcher === 'subgroup') {
groupMatch =
groupId === contentGroupId || `${groupId}.`.startsWith(contentGroupId);
} else {
groupMatch = groupId === contentGroupId;
}

// artifact matching (optional)
let artifactMatch = true;
if (groupMatch && contentArtifactId) {
if (matcher === 'regex') {
artifactMatch = regEx(contentArtifactId).test(artifactId);
} else {
artifactMatch = artifactId === contentArtifactId;
}
}

// version matching (optional)
let versionMatch = true;
if (groupMatch && artifactMatch && contentVersion && dep.currentValue) {
if (matcher === 'regex') {
versionMatch = regEx(contentVersion).test(dep.currentValue);
} else {
// contentVersion can be an exact version or a gradle-supported version range
versionMatch = gradleVersioning.matches(
dep.currentValue,
contentVersion,
);
}
}

const isMatch = groupMatch && artifactMatch && versionMatch;
if (mode === 'include') {
hasIncludes = true;
if (isMatch) {
matchesInclude = true;
}
} else if (mode === 'exclude') {
hasExcludes = true;
if (isMatch) {
matchesExclude = true;
}
}
}

if (hasIncludes && hasExcludes) {
// if both includes and excludes exist, dep must match include and not match exclude
return matchesInclude && !matchesExclude;
} else if (hasIncludes) {
// if only includes exist, dep must match at least one include
return matchesInclude;
} else if (hasExcludes) {
// if only excludes exist, dep must not match any exclude
return !matchesExclude;
}

// by default, repositories include everything and exclude nothing
return true;
}

function getRegistryUrlsForDep(
packageRegistries: PackageRegistry[],
dep: PackageDependency<GradleManagerData>,
Expand All @@ -52,6 +139,7 @@ function getRegistryUrlsForDep(

const registryUrls = packageRegistries
.filter((item) => item.scope === scope)
.filter((item) => matchesContentDescriptor(dep, item.content))
.map((item) => item.registryUrl);

if (!registryUrls.length && scope === 'plugin') {
Expand Down
Loading

0 comments on commit aae045e

Please sign in to comment.