Skip to content

Commit ba9faac

Browse files
committed
feat(ort-config): Extract a generic GitPackageCurationProvider
Extract a generic `GitPackageCurationProvider` from the `OrtConfigPackageCurationProvider` and make the latter inherit from the new one. The new provider allows to use any Git repository as a source for package curation files. For now, the provider requires the same file layout as the `OrtConfigPackageCurationProvider`, this could be made configurable if required later on. Signed-off-by: Martin Nonnenmacher <martin.nonnenmacher@doubleopen.io>
1 parent b1b82c2 commit ba9faac

3 files changed

Lines changed: 145 additions & 70 deletions

File tree

plugins/package-curation-providers/ort-config/build.gradle.kts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,8 @@ plugins {
2525
dependencies {
2626
api(projects.plugins.packageCurationProviders.packageCurationProviderApi)
2727

28-
implementation(projects.downloader)
28+
implementation(projects.plugins.versionControlSystems.gitVersionControlSystem)
2929
implementation(projects.utils.commonUtils)
3030

31-
funTestImplementation(projects.plugins.versionControlSystems.gitVersionControlSystem)
32-
3331
ksp(projects.plugins.packageCurationProviders.packageCurationProviderApi)
3432
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Copyright (C) 2026 The ORT Project Copyright Holders <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
package org.ossreviewtoolkit.plugins.packagecurationproviders.ortconfig
21+
22+
import java.io.File
23+
import java.io.IOException
24+
25+
import org.apache.logging.log4j.kotlin.logger
26+
27+
import org.ossreviewtoolkit.model.Identifier
28+
import org.ossreviewtoolkit.model.Package
29+
import org.ossreviewtoolkit.model.PackageCuration
30+
import org.ossreviewtoolkit.model.VcsInfo
31+
import org.ossreviewtoolkit.model.VcsType
32+
import org.ossreviewtoolkit.model.readValue
33+
import org.ossreviewtoolkit.plugins.api.OrtPlugin
34+
import org.ossreviewtoolkit.plugins.api.OrtPluginOption
35+
import org.ossreviewtoolkit.plugins.api.PluginDescriptor
36+
import org.ossreviewtoolkit.plugins.packagecurationproviders.api.PackageCurationProvider
37+
import org.ossreviewtoolkit.plugins.packagecurationproviders.api.PackageCurationProviderFactory
38+
import org.ossreviewtoolkit.plugins.versioncontrolsystems.git.GitFactory
39+
import org.ossreviewtoolkit.utils.common.div
40+
import org.ossreviewtoolkit.utils.common.encodeOr
41+
import org.ossreviewtoolkit.utils.common.fileSystemEncode
42+
import org.ossreviewtoolkit.utils.common.safeMkdirs
43+
import org.ossreviewtoolkit.utils.ort.ortDataDirectory
44+
45+
data class GitPackageCurationProviderConfig(
46+
/** The URL of the repository containing the curations. */
47+
val repositoryUrl: String,
48+
49+
/** The optional revision to use. If not specified, the default branch is used. */
50+
val revision: String?,
51+
52+
/** The path that contains the package curations. */
53+
@OrtPluginOption(defaultValue = "curations")
54+
val path: String
55+
)
56+
57+
/**
58+
* A [PackageCurationProvider] that loads [PackageCuration]s from the configured Git repository.
59+
*
60+
* ### Path layout
61+
*
62+
* The provider requires that the curation files follow the same path layout as in the
63+
* [ort-config repository](https://github.com/oss-review-toolkit/ort-config#curations):
64+
*
65+
* * Files with curations for a specific package must be located at `[type]/[namespace]/[name].yml`, based on the
66+
* identifier of the package. If a component of the identifier is empty, `_` is used as a placeholder. For example,
67+
* for the package `NuGet::Azure.Core:1.2.0`, the curation file must be located at `NuGet/_/Azure.Core.yml`.
68+
* * Files with curations that match all packages within a namespace must be located at `[type]/[namespace]/_.yml`.
69+
*
70+
* Namespace-scoped curations are loaded before package-scoped curations, so that the latter can override the former.
71+
*/
72+
@OrtPlugin(
73+
displayName = "Git",
74+
summary = "A package curation provider that loads package curations from a Git repository.",
75+
factory = PackageCurationProviderFactory::class
76+
)
77+
open class GitPackageCurationProvider(
78+
override val descriptor: PluginDescriptor = GitPackageCurationProviderFactory.descriptor,
79+
private val config: GitPackageCurationProviderConfig
80+
) : PackageCurationProvider {
81+
init {
82+
require(config.repositoryUrl.isNotBlank()) { "The repository URL must not be blank." }
83+
}
84+
85+
private val repositoryDir by lazy {
86+
// Use a stable cache path to clone the repository to speed up subsequent runs.
87+
(ortDataDirectory / "cache" / "git-package-curation-provider" / config.repositoryUrl.fileSystemEncode()).also {
88+
updateRepository(it)
89+
}
90+
}
91+
92+
private val curationsDir by lazy { repositoryDir / config.path }
93+
94+
override fun getCurationsFor(packages: Collection<Package>): Set<PackageCuration> =
95+
packages.flatMapTo(mutableSetOf()) { pkg -> getCurationsFor(pkg.id) }
96+
97+
private fun getCurationsFor(pkgId: Identifier): List<PackageCuration> {
98+
// The Git repository has to follow path layout conventions, so curations can be looked up directly.
99+
val packageCurationsFile = curationsDir / pkgId.toCurationPath()
100+
101+
// Also consider curations for all packages in a namespace.
102+
val namespaceCurationsFile = packageCurationsFile.resolveSibling("_.yml")
103+
104+
// Return namespace-level curations before package-level curations to allow overriding the former.
105+
return listOf(namespaceCurationsFile, packageCurationsFile).filter { it.isFile }.flatMap { file ->
106+
runCatching {
107+
file.readValue<List<PackageCuration>>().filter { it.isApplicable(pkgId) }
108+
}.getOrElse {
109+
throw IOException("Failed parsing package curation from '${file.absolutePath}'.", it)
110+
}
111+
}
112+
}
113+
114+
private fun updateRepository(dir: File) {
115+
val vcsInfo = VcsInfo.EMPTY.copy(type = VcsType.GIT, url = config.repositoryUrl)
116+
dir.safeMkdirs()
117+
118+
GitFactory.create().apply {
119+
val workingTree = initWorkingTree(dir, vcsInfo)
120+
val revision = config.revision ?: getDefaultBranchName(config.repositoryUrl)
121+
val clonedRevision = updateWorkingTree(workingTree, revision).getOrThrow()
122+
123+
logger.info {
124+
"Successfully cloned $clonedRevision ($revision) from ${config.repositoryUrl}."
125+
}
126+
}
127+
}
128+
}
129+
130+
/**
131+
* The path must be aligned with the
132+
* [conventions for the ort-config repository](https://github.com/oss-review-toolkit/ort-config#curations).
133+
*/
134+
fun Identifier.toCurationPath() = "${type.encodeOr("_")}/${namespace.encodeOr("_")}/${name.encodeOr("_")}.yml"

plugins/package-curation-providers/ort-config/src/main/kotlin/OrtConfigPackageCurationProvider.kt

Lines changed: 10 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,11 @@
1919

2020
package org.ossreviewtoolkit.plugins.packagecurationproviders.ortconfig
2121

22-
import java.io.File
23-
import java.io.IOException
24-
25-
import org.apache.logging.log4j.kotlin.logger
26-
27-
import org.ossreviewtoolkit.downloader.VersionControlSystem
28-
import org.ossreviewtoolkit.model.Identifier
29-
import org.ossreviewtoolkit.model.Package
3022
import org.ossreviewtoolkit.model.PackageCuration
31-
import org.ossreviewtoolkit.model.VcsInfo
32-
import org.ossreviewtoolkit.model.VcsType
33-
import org.ossreviewtoolkit.model.readValue
3423
import org.ossreviewtoolkit.plugins.api.OrtPlugin
3524
import org.ossreviewtoolkit.plugins.api.PluginDescriptor
3625
import org.ossreviewtoolkit.plugins.packagecurationproviders.api.PackageCurationProvider
3726
import org.ossreviewtoolkit.plugins.packagecurationproviders.api.PackageCurationProviderFactory
38-
import org.ossreviewtoolkit.utils.common.div
39-
import org.ossreviewtoolkit.utils.common.encodeOr
40-
import org.ossreviewtoolkit.utils.common.safeMkdirs
41-
import org.ossreviewtoolkit.utils.ort.ortDataDirectory
4227

4328
private const val ORT_CONFIG_REPOSITORY_BRANCH = "main"
4429
private const val ORT_CONFIG_REPOSITORY_URL = "https://github.com/oss-review-toolkit/ort-config.git"
@@ -53,55 +38,13 @@ private const val ORT_CONFIG_REPOSITORY_URL = "https://github.com/oss-review-too
5338
summary = "A package curation provider that loads package curations from the ort-config repository.",
5439
factory = PackageCurationProviderFactory::class
5540
)
56-
open class OrtConfigPackageCurationProvider(
57-
override val descriptor: PluginDescriptor = OrtConfigPackageCurationProviderFactory.descriptor
58-
) : PackageCurationProvider {
59-
private val curationsDir by lazy {
60-
ortDataDirectory.resolve("ort-config").also {
61-
updateOrtConfig(it)
62-
}
63-
}
64-
65-
override fun getCurationsFor(packages: Collection<Package>): Set<PackageCuration> =
66-
packages.flatMapTo(mutableSetOf()) { pkg -> getCurationsFor(pkg.id) }
67-
68-
private fun getCurationsFor(pkgId: Identifier): List<PackageCuration> {
69-
// The ORT config repository follows path layout conventions, so curations can be looked up directly.
70-
val packageCurationsFile = curationsDir / "curations" / pkgId.toCurationPath()
71-
72-
// Also consider curations for all packages in a namespace.
73-
val namespaceCurationsFile = packageCurationsFile.resolveSibling("_.yml")
74-
75-
// Return namespace-level curations before package-level curations to allow overriding the former.
76-
return listOf(namespaceCurationsFile, packageCurationsFile).filter { it.isFile }.flatMap { file ->
77-
runCatching {
78-
file.readValue<List<PackageCuration>>().filter { it.isApplicable(pkgId) }
79-
}.getOrElse {
80-
throw IOException("Failed parsing package curation from '${file.absolutePath}'.", it)
81-
}
82-
}
83-
}
84-
}
85-
86-
/**
87-
* The path must be aligned with the
88-
* [conventions for the ort-config repository](https://github.com/oss-review-toolkit/ort-config#curations).
89-
*/
90-
fun Identifier.toCurationPath() = "${type.encodeOr("_")}/${namespace.encodeOr("_")}/${name.encodeOr("_")}.yml"
91-
92-
private fun updateOrtConfig(dir: File) {
93-
val vcsInfo = VcsInfo.EMPTY.copy(type = VcsType.GIT, url = ORT_CONFIG_REPOSITORY_URL)
94-
val vcs = checkNotNull(VersionControlSystem.forType(vcsInfo.type)) {
95-
"No applicable VersionControlSystem implementation found for ${vcsInfo.type}."
96-
}
97-
98-
dir.safeMkdirs()
99-
100-
vcs.apply {
101-
val workingTree = initWorkingTree(dir, vcsInfo)
102-
val revision = updateWorkingTree(workingTree, ORT_CONFIG_REPOSITORY_BRANCH).getOrThrow()
103-
logger.info {
104-
"Successfully cloned $revision from $ORT_CONFIG_REPOSITORY_URL."
105-
}
106-
}
107-
}
41+
class OrtConfigPackageCurationProvider(
42+
descriptor: PluginDescriptor = OrtConfigPackageCurationProviderFactory.descriptor
43+
) : GitPackageCurationProvider(
44+
descriptor = descriptor,
45+
config = GitPackageCurationProviderConfig(
46+
repositoryUrl = ORT_CONFIG_REPOSITORY_URL,
47+
revision = ORT_CONFIG_REPOSITORY_BRANCH,
48+
path = "curations"
49+
)
50+
)

0 commit comments

Comments
 (0)