diff --git a/gradle-plugin/plugin/src/main/kotlin/io/github/gmazzo/codeowners/CodeOwnersExtension.kt b/gradle-plugin/plugin/src/main/kotlin/io/github/gmazzo/codeowners/CodeOwnersExtension.kt index e64b6b3..71e20f5 100644 --- a/gradle-plugin/plugin/src/main/kotlin/io/github/gmazzo/codeowners/CodeOwnersExtension.kt +++ b/gradle-plugin/plugin/src/main/kotlin/io/github/gmazzo/codeowners/CodeOwnersExtension.kt @@ -17,11 +17,10 @@ interface CodeOwnersExtension { val codeOwnersFile : RegularFileProperty /** - * Where if `core` dependency (providing the [io.github.gmazzo.codeowners.codeOwnersOf] API) should be added to - * the `implementation` classpath. + * If Kotlin classes runtime available (accessed from [io.github.gmazzo.codeowners.codeOwnersOf] API). * - * Defaults to `codeowners.default.dependency` Gradle property (`true` if missing). + * If `false` only [CodeOwnersReportTask] tasks will be added to the projects. */ - val addCoreDependency: Property + val enableRuntimeSupport: Property } diff --git a/gradle-plugin/plugin/src/main/kotlin/io/github/gmazzo/codeowners/CodeOwnersPlugin.kt b/gradle-plugin/plugin/src/main/kotlin/io/github/gmazzo/codeowners/CodeOwnersPlugin.kt index fbd6a80..c45f541 100644 --- a/gradle-plugin/plugin/src/main/kotlin/io/github/gmazzo/codeowners/CodeOwnersPlugin.kt +++ b/gradle-plugin/plugin/src/main/kotlin/io/github/gmazzo/codeowners/CodeOwnersPlugin.kt @@ -13,6 +13,7 @@ import org.gradle.kotlin.dsl.codeOwners import org.gradle.kotlin.dsl.create import org.gradle.kotlin.dsl.findByType import org.gradle.kotlin.dsl.newInstance +import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.the import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension @@ -45,80 +46,117 @@ class CodeOwnersPlugin : KotlinCompilerPluginSupportPlugin { } override fun apply(target: Project): Unit = with(target) { - val extension = generateSequence(target) { it.parent } - .mapNotNull { it.extensions.findByType() } - .firstOrNull() - ?.also { extensions.add("codeOwners", it) } - ?: createExtension() + val extension = createExtension() + configureKotlinExtension(extension) + } + + private fun Project.configureKotlinExtension( + extension: CodeOwnersExtension, + ) = plugins.withType { + val reportTask = tasks.register("codeOwnersReport") { + group = "CodeOwners" + description = "Generate CODEOWNERS report for all targets" + + rootDirectory.set(extension.rootDirectory) + codeOwnersFile.set(extension.codeOwnersFile) + codeOwnersReportHeader.set("CodeOwners of module ${project.name}") + codeOwnersReportFile.set(layout.buildDirectory.file("reports/codeowners/${pathAsFileName}.properties")) + } + + fun KotlinTarget.configure(single: Boolean) { + val targetName = this@configure.name + + val targetReportTask = + if (single) null + else tasks.register("${targetName}CodeOwnersReport") { + group = "CodeOwners" + description = "Generate CODEOWNERS report for '$targetName' target" + + rootDirectory.set(extension.rootDirectory) + codeOwnersFile.set(extension.codeOwnersFile) + codeOwnersReportHeader.set("CodeOwners of $targetName of module ${project.path}") + codeOwnersReportFile.set(layout.buildDirectory.file("reports/codeowners/${project.pathAsFileName}/$targetName.properties")) + } + + val targetExtension = objects.newInstance() + .also(::codeOwners.setter) - plugins.withType { - fun KotlinTarget.configure() { - val targetExtension = objects.newInstance() + targetExtension.enabled + .convention(extension.enableRuntimeSupport) + .finalizeValueOnRead() + + compilations.configureEach { + val compilationExtension = objects.newInstance() .also(::codeOwners.setter) - targetExtension.enabled - .convention(true) + compilationExtension.enabled + .convention(targetExtension.enabled) .finalizeValueOnRead() - compilations.configureEach { - val compilationExtension = objects.newInstance() - .also(::codeOwners.setter) - - compilationExtension.enabled - .convention(targetExtension.enabled) - .finalizeValueOnRead() + addCodeDependency(compilationExtension, defaultSourceSet.implementationConfigurationName) - addCodeDependency(extension, compilationExtension, defaultSourceSet.implementationConfigurationName) + listOfNotNull(reportTask, targetReportTask).forEach { + it.configure { + sources.from(allKotlinSourceSets.map { it.kotlin }) + } } } + } - when (val kotlin = extensions.getByName("kotlin")) { - is KotlinSingleTargetExtension<*> -> kotlin.target.configure() - is KotlinTargetsContainer -> kotlin.targets.configureEach { configure() } - } + when (val kotlin = extensions.getByName("kotlin")) { + is KotlinSingleTargetExtension<*> -> kotlin.target.configure(single = true) + is KotlinTargetsContainer -> kotlin.targets.configureEach { configure(single = false) } } } - private fun Project.createExtension() = extensions.create("codeOwners").apply { - rootDirectory - .convention(layout.projectDirectory) - .finalizeValueOnRead() - - val defaultLocations = files( - "CODEOWNERS", - ".github/CODEOWNERS", - ".gitlab/CODEOWNERS", - "docs/CODEOWNERS", - ) - - val defaultFile = lazy { - defaultLocations.asFileTree.singleOrNull() ?: error(defaultLocations.joinToString( - prefix = "No CODEOWNERS file found! Default locations:\n", - separator = "\n" - ) { "- ${it.toRelativeString(rootDir)}" }) - } + private fun Project.createExtension(): CodeOwnersExtension { + val parentExtension = generateSequence(parent) { it.parent } + .mapNotNull { it.extensions.findByType() } + .firstOrNull() + + return extensions.create("codeOwners").apply { + + rootDirectory + .value(parentExtension?.rootDirectory?.orNull) + .convention(layout.projectDirectory) + .finalizeValueOnRead() + + val defaultLocations = files( + "CODEOWNERS", + ".github/CODEOWNERS", + ".gitlab/CODEOWNERS", + "docs/CODEOWNERS", + ) + + val defaultFile = lazy { + defaultLocations.asFileTree.singleOrNull() ?: error(defaultLocations.joinToString( + prefix = "No CODEOWNERS file found! Default locations:\n", + separator = "\n" + ) { "- ${it.toRelativeString(rootDir)}" }) + } - codeOwnersFile - .convention(layout.file(provider(defaultFile::value))) - .finalizeValueOnRead() + codeOwnersFile + .value(parentExtension?.codeOwnersFile?.orNull) + .convention(layout.file(provider(defaultFile::value))) + .finalizeValueOnRead() - addCoreDependency - .convention(findProperty("codeowners.default.dependency")?.toString()?.toBoolean() != false) - .finalizeValueOnRead() + enableRuntimeSupport + .value(parentExtension?.enableRuntimeSupport?.orNull) + .convention(true) + .finalizeValueOnRead() + } } private fun Project.addCodeDependency( - extension: CodeOwnersExtension, target: CodeOwnersCompilationExtension, configurationName: String, ) { dependencies.addProvider( configurationName, - extension.addCoreDependency.and(target.enabled) - .map { if (it) CORE_DEPENDENCY else files() }) + target.enabled.map { if (it) CORE_DEPENDENCY else files() }) } - private fun Provider.and(vararg others: Provider) = - sequenceOf(this, *others).reduce { acc, it -> acc.zip(it, Boolean::and) } + private val Project.pathAsFileName + get() = path.removePrefix(":").replace(':', '-') } diff --git a/gradle-plugin/plugin/src/main/kotlin/io/github/gmazzo/codeowners/CodeOwnersReportTask.kt b/gradle-plugin/plugin/src/main/kotlin/io/github/gmazzo/codeowners/CodeOwnersReportTask.kt new file mode 100644 index 0000000..8a316b2 --- /dev/null +++ b/gradle-plugin/plugin/src/main/kotlin/io/github/gmazzo/codeowners/CodeOwnersReportTask.kt @@ -0,0 +1,85 @@ +package io.github.gmazzo.codeowners + +import io.github.gmazzo.codeowners.matcher.CodeOwnersFile +import io.github.gmazzo.codeowners.matcher.CodeOwnersMatcher +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileTree +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.IgnoreEmptyDirectories +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.SkipWhenEmpty +import org.gradle.api.tasks.TaskAction +import org.jetbrains.kotlin.konan.file.use +import java.util.Properties + +@CacheableTask +@Suppress("LeakingThis") +abstract class CodeOwnersReportTask : DefaultTask() { + + @get:Internal + abstract val rootDirectory: DirectoryProperty + + /** + * Helper input to declare that we only care about paths and not file contents on [rootDirectory] and [sources] + * + * [Incorrect use of the `@Input` annotation](https://docs.gradle.org/7.6/userguide/validation_problems.html#incorrect_use_of_input_annotation) + */ + @get:Input + internal val rootDirectoryPath = + rootDirectory.map { it.asFile.toRelativeString(project.rootDir) } + + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val codeOwnersFile: RegularFileProperty + + @get:Internal + abstract val sources: ConfigurableFileCollection + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:IgnoreEmptyDirectories + @get:SkipWhenEmpty + internal val sourcesFiles: FileTree = sources.asFileTree + + @get:Input + @get:Optional + abstract val codeOwnersReportHeader: Property + + @get:OutputFile + abstract val codeOwnersReportFile: RegularFileProperty + + @TaskAction + fun generateCodeOwnersInfo() { + val root = rootDirectory.asFile.get() + val matcher = CodeOwnersMatcher( + root, + codeOwnersFile.asFile.get().useLines { CodeOwnersFile(it) } + ) + val report = Properties() + + sourcesFiles.forEach { file -> + val owners = matcher.ownerOf(file) + + if (owners != null) { + val relativePath = file.toRelativeString(root) + + // FIXME output is not the wanted one + report[relativePath] = owners.joinToString(separator = ",") + } + } + + codeOwnersReportFile.asFile.get().writer().use { report.store(it, codeOwnersReportHeader.orNull) } + } + +}