Skip to content

Instantly share code, notes, and snippets.

@kozaxinan
Last active July 10, 2020 06:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kozaxinan/7762a22eed488e4634199e293f5f2e35 to your computer and use it in GitHub Desktop.
Save kozaxinan/7762a22eed488e4634199e293f5f2e35 to your computer and use it in GitHub Desktop.
dependencies {
implementation "com.android.tools.build:gradle:3.6.3"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // https://issuetracker.google.com/issues/123491449
implementation "org.eclipse.jgit:org.eclipse.jgit:5.5.1.201910021850-r"
}
package de.is24.git.diff
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.AbstractTreeIterator
import org.eclipse.jgit.treewalk.CanonicalTreeParser
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.UnknownConfigurationException
import org.gradle.api.internal.artifacts.dependencies.DefaultProjectDependency
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.testing.Test
import java.io.File
abstract class TestDiff : DefaultTask() { // Keep the file open otherwise gradle won't be able to generate the proxy class.
private val branchProperty = "branch"
private val nameOftestTasks = arrayOf("test", "testDebugUnitTest")
init {
if (project.hasProperty(branchProperty)) {
val branchName = project
.property(branchProperty)
.toString()
println("Running tests for changes of $branchName")
val affectedProjects = projectsAffectedByBranch(branchName, project.rootProject)
val testTasks = mutableListOf<Task>()
affectedProjects.forEach { project ->
val testTasksOfModule = project.tasks
.withType(Test::class.java)
.filter { task ->
task.name in nameOftestTasks
}
val testDebugUnitTest = testTasksOfModule.find { it.name == "testDebugUnitTest" }
if (testDebugUnitTest != null) {
println("${project.name}:testDebugUnitTest module will be tested")
testTasks += testDebugUnitTest
} else {
testTasksOfModule.forEach {
println("${project.name}:${it.name} module will be tested")
}
testTasks += testTasksOfModule
}
}
if (testTasks.isNotEmpty()) {
dependsOn(testTasks)
} else {
println("Full codebase will be tested")
dependsOn("ciUnitTests")
}
}
}
@TaskAction // Marks a function as the action to run when the task is executed.
fun run() {
if (!project.hasProperty(branchProperty)) {
throw IllegalArgumentException("please specify branch property with -P$branchProperty=<BranchName>")
}
}
private fun projectsAffectedByBranch(branchName: String, rootProject: Project): Collection<Project> {
val diffFilePaths = getDiffFilePaths(rootProject, branchName)
val affectedProjects = getProjectsForFilePaths(rootProject, diffFilePaths)
return affectedProjects
.flatMap { getDependantProjects(rootProject.subprojects, it) + it }
.distinct()
}
private fun getGitReferenceTree(repository: Repository, ref: String): AbstractTreeIterator {
val head = repository.exactRef(ref)
RevWalk(repository).use { walk ->
val commit = walk.parseCommit(head.objectId)
val tree = walk.parseTree(commit.tree.id)
val treeParser = CanonicalTreeParser()
repository
.newObjectReader()
.use { reader -> treeParser.reset(reader, tree.id) }
walk.dispose()
return treeParser
}
}
private fun getDiffFilePaths(rootProject: Project, branchName: String): Iterable<String> {
val git = Git.open(File(rootProject.projectDir.path))
val masterTree = getGitReferenceTree(git.repository, "refs/remotes/origin/develop")
val branchTree = getGitReferenceTree(git.repository, "refs/remotes/origin/$branchName")
val diffEntries = git
.diff()
.setOldTree(masterTree)
.setNewTree(branchTree)
.call()
return diffEntries.flatMap { diffEntry -> listOf(diffEntry.oldPath, diffEntry.newPath) }
}
private fun getProjectsForFilePaths(rootProject: Project, filePaths: Iterable<String>): Iterable<Project> =
rootProject.subprojects.filter { subproject ->
filePaths.find { filePath -> filePath.startsWith(subproject.projectDir.toRelativeString(rootProject.projectDir)) } != null
}
private fun getDependantProjects(allProjects: Iterable<Project>, dependencyProject: Project): Iterable<Project> {
val dependants = allProjects
.filter { subproject ->
try {
val implementationDependencies: Set<Dependency> = try {
subproject
.configurations
.getByName("implementation")
.dependencies
} catch (ignored: UnknownConfigurationException) {
emptySet()
}
val apiDependencies: Set<Dependency> = try {
subproject
.configurations
.getByName("api")
.dependencies
} catch (ignored: UnknownConfigurationException) {
emptySet()
}
val combinedDepSet = implementationDependencies + apiDependencies
val dependencies = combinedDepSet
.distinct()
.filterIsInstance<DefaultProjectDependency>()
.find { dependency -> dependency.dependencyProject == dependencyProject }
dependencies != null
} catch (exception: UnknownConfigurationException) {
false
}
}
.distinct()
return dependants + dependants.flatMap { getDependantProjects(allProjects, it) }
}
}
tasks.register("ciUnitTestsForBranch", TestDiff) {
group = 'Continuous Integration'
description = 'Executes unit test for branch'
}
./gradlew ciUnitTestsForBranch -Pbranch=\"${getGitBranchName()}\"
(Branch name of current pr, it is `env.BRANCH_NAME` for our jenkins setup)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment