Skip to content

Commit

Permalink
Added CodeOwnersReportTask
Browse files Browse the repository at this point in the history
  • Loading branch information
gmazzo committed Dec 11, 2023
1 parent 2818c30 commit e06666a
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean>
val enableRuntimeSupport: Property<Boolean>

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<CodeOwnersExtension>() }
.firstOrNull()
?.also { extensions.add("codeOwners", it) }
?: createExtension()
val extension = createExtension()
configureKotlinExtension(extension)
}

private fun Project.configureKotlinExtension(
extension: CodeOwnersExtension,
) = plugins.withType<KotlinBasePlugin> {
val reportTask = tasks.register<CodeOwnersReportTask>("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<CodeOwnersReportTask>("${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<CodeOwnersCompilationExtension>()
.also(::codeOwners.setter)

plugins.withType<KotlinBasePlugin> {
fun KotlinTarget.configure() {
val targetExtension = objects.newInstance<CodeOwnersCompilationExtension>()
targetExtension.enabled
.convention(extension.enableRuntimeSupport)
.finalizeValueOnRead()

compilations.configureEach {
val compilationExtension = objects.newInstance<CodeOwnersCompilationExtension>()
.also(::codeOwners.setter)

targetExtension.enabled
.convention(true)
compilationExtension.enabled
.convention(targetExtension.enabled)
.finalizeValueOnRead()

compilations.configureEach {
val compilationExtension = objects.newInstance<CodeOwnersCompilationExtension>()
.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<CodeOwnersExtension>("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<CodeOwnersExtension>() }
.firstOrNull()

return extensions.create<CodeOwnersExtension>("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<Boolean>.and(vararg others: Provider<Boolean>) =
sequenceOf(this, *others).reduce { acc, it -> acc.zip(it, Boolean::and) }
private val Project.pathAsFileName
get() = path.removePrefix(":").replace(':', '-')

}
Original file line number Diff line number Diff line change
@@ -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<String>

@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) }
}

}

0 comments on commit e06666a

Please sign in to comment.