Merge "Create reportModifications task" into oc-mr1-support-27.0-dev
diff --git a/app-toolkit/dependencies.gradle b/app-toolkit/dependencies.gradle
index cb68089..eb90b12 100644
--- a/app-toolkit/dependencies.gradle
+++ b/app-toolkit/dependencies.gradle
@@ -22,7 +22,6 @@
     ffLibs = libs
 }
 def ffVersions = [:]
-ffVersions.kotlin = "1.1.3"
 ffVersions.auto_common = "0.6"
 ffVersions.javapoet = "1.8.0"
 ffVersions.compile_testing = "0.11"
@@ -40,10 +39,6 @@
 ffVersions.guava = "21.0"
 ffVersions.jsr250 = "1.2"
 
-ffLibs.kotlin = [
-        stdlib : "org.jetbrains.kotlin:kotlin-stdlib:$ffVersions.kotlin",
-        gradle_plugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$ffVersions.kotlin"
-]
 ffLibs.auto_common = "com.google.auto:auto-common:$ffVersions.auto_common"
 ffLibs.apache = [
     commons : [
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index 9f648d9..11b9a27 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -1,13 +1,21 @@
-apply plugin: 'groovy'
-apply plugin: 'java'
-
-apply from: "dependencies.gradle"
+buildscript {
+    ext.supportRootFolder = project.projectDir.getParentFile()
+    apply from: 'repos.gradle'
+    repos.addMavenRepositories(repositories)
+    apply from: "dependencies.gradle"
+    dependencies {
+        classpath libs.kotlin.gradle_plugin
+    }
+}
 
 ext.supportRootFolder = project.projectDir.getParentFile()
 apply from: 'repos.gradle'
-
 repos.addMavenRepositories(repositories)
 
+apply plugin: 'groovy'
+apply plugin: 'java'
+apply plugin: 'kotlin'
+
 dependencies {
     compile libs.gradle
     compile libs.jacoco
diff --git a/buildSrc/dependencies.gradle b/buildSrc/dependencies.gradle
index 124f046..ef538d1 100644
--- a/buildSrc/dependencies.gradle
+++ b/buildSrc/dependencies.gradle
@@ -26,6 +26,11 @@
 libs.espresso_contrib = 'com.android.support.test.espresso:espresso-contrib:3.0.1'
 libs.jacoco = 'org.jacoco:org.jacoco.core:0.7.8'
 
+libs.kotlin = [
+    stdlib : "org.jetbrains.kotlin:kotlin-stdlib:1.1.3",
+    gradle_plugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:1.1.3"
+]
+
 def androidPluginVersionOverride = System.getenv("GRADLE_PLUGIN_VERSION")
 
 if (androidPluginVersionOverride != null) {
diff --git a/buildSrc/init.gradle b/buildSrc/init.gradle
index a81a2a9..b3cd58f 100644
--- a/buildSrc/init.gradle
+++ b/buildSrc/init.gradle
@@ -332,3 +332,6 @@
 ext.init.setupRelease = this.&setupRelease
 ext.init.configureSubProjects = this.&configureSubProjects
 ext.init.configureBuildOnServer = this.&configureBuildOnServer
+ext.init.enableModificationsReporter = this.&enableModificationsReporter
+
+apply from: "${ext.supportRootFolder}/buildSrc/modifications_reporter.gradle"
\ No newline at end of file
diff --git a/buildSrc/modifications_reporter.gradle b/buildSrc/modifications_reporter.gradle
new file mode 100644
index 0000000..b8fcbc5
--- /dev/null
+++ b/buildSrc/modifications_reporter.gradle
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.support.ModifiedProjectsReporter
+
+task(reportModifications) {
+    doLast{
+        if (!rootProject.hasProperty("SHAs")) {
+            throw new GradleException("You must specify SHAs range: -PSHAs=<rev1>...<rev2>")
+        }
+        def range = SHAs.tokenize('...')
+        if (range.size() != 2) {
+            throw new GradleException("You must specify SHAs in format: <rev1>...<rev2>")
+        }
+        printModifications(range[0], range[1])
+    }
+}
+
+def printModifications(sha1, sha2) {
+    def modificationsReporter = new ModifiedProjectsReporter()
+    modificationsReporter.printModifiedProjects(ext.supportRootFolder, sha1, sha2, rootProject)
+}
diff --git a/buildSrc/src/main/kotlin/android/support/ModifiedProjectsReporter.kt b/buildSrc/src/main/kotlin/android/support/ModifiedProjectsReporter.kt
new file mode 100644
index 0000000..1adbd0b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/android/support/ModifiedProjectsReporter.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support
+
+import org.gradle.api.GradleException
+import java.io.File
+import java.util.concurrent.Executors
+import java.util.concurrent.Future
+import java.util.concurrent.TimeUnit
+import org.gradle.api.Project
+
+
+@Suppress("unused")
+class ModifiedProjectsReporter {
+
+    private val ioExecutor = Executors.newFixedThreadPool(5)
+    private fun <T> ioThread(f: () -> T): Future<T> = ioExecutor.submit(f)
+
+    fun printModifiedProjects(gitRoot: File,
+                              shaFirst: String, shaLast: String, rootProject: Project) {
+
+        val modifiedRootProjects = rootProject.subprojects
+                .filter { project ->
+                    project.hasMavenArtifact()
+                            && project.projectDir.canonicalPath.startsWith(gitRoot.canonicalPath)
+                }
+                .map { project ->
+                    project to hasModification(shaFirst, shaLast, project.projectDir)
+                }
+                .associate { (project, future) -> project to future.get() }
+                .filterValues { it }
+                .keys
+        val graph = PublicProjectsGraph(rootProject)
+        val modifiedProjects  = markDependentProjects(graph, modifiedRootProjects)
+
+        if (modifiedProjects.isEmpty()) {
+            println("No modifications found in public modules")
+            return
+        }
+
+        println("Modified Projects:")
+        modifiedProjects.forEach {
+            println("   $it")
+        }
+    }
+
+    private fun markDependentProjects(graph: PublicProjectsGraph,
+                                      modified: Set<Project>): Set<Project> {
+        val result = mutableSetOf<Project>()
+
+        fun visit(project: Project) {
+            if (project in result) {
+                return
+            }
+            result.add(project)
+            graph.dependents(project).forEach(::visit)
+        }
+        modified.forEach(::visit)
+        return result
+    }
+
+    private fun hasModification(shaFirst: String, shaLast: String, projectDir: File) = ioThread {
+        // sample: git log 367f3f6c46af1591..3636a95826a9 -- lifecycle/
+        val process = ProcessBuilder("git", "log", "$shaFirst..$shaLast", "--",
+                projectDir.absolutePath).start()
+
+        val awaitResult = process.waitFor(1, TimeUnit.SECONDS)
+        if (!awaitResult) {
+            throw GradleException("Failed to await for git")
+        }
+        if (process.exitValue() != 0) {
+            val errorMessage = process.errorStream.bufferedReader().use { it.readText() }
+            throw GradleException("Git command failed: $errorMessage")
+        }
+        process.inputStream.bufferedReader().use { !it.readLine().isNullOrEmpty() }
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/android/support/PublicProjectsGraph.kt b/buildSrc/src/main/kotlin/android/support/PublicProjectsGraph.kt
new file mode 100644
index 0000000..5f8e952
--- /dev/null
+++ b/buildSrc/src/main/kotlin/android/support/PublicProjectsGraph.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support
+
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.artifacts.ProjectDependency
+import org.gradle.api.tasks.Upload
+
+class PublicProjectsGraph(rootProject: Project) {
+    private val graph: MutableMap<Project, MutableSet<Project>> = mutableMapOf()
+
+    init {
+        val logger = rootProject.logger
+        rootProject.subprojects.filter { project ->
+            if (project.subprojects.size != 0) {
+                logger.info("PublicModuleGraph: $project ignored because it's top level")
+                false
+            } else {
+                val hasMavenArtifact = project.hasMavenArtifact()
+                if (!hasMavenArtifact) {
+                    logger.info("PublicModuleGraph: $project ignored because " +
+                            "it doesn't have upload task")
+                }
+                hasMavenArtifact
+            }
+        }.forEach { project ->
+            addDependenciesFrom(project, "compile")
+            addDependenciesFrom(project, "api")
+            addDependenciesFrom(project, "implementation")
+        }
+    }
+
+    fun addDependenciesFrom(project: Project, configName: String) {
+        val config: Configuration = project.configurations.findByName(configName) ?: return
+        config.dependencies.withType(ProjectDependency::class.java).forEach { dep ->
+            graph.getOrPut(dep.dependencyProject, { mutableSetOf<Project>() }).add(project)
+        }
+    }
+
+    fun dependents(project: Project): Set<Project> = graph.getOrDefault(project, mutableSetOf())
+}
+
+fun Project.hasMavenArtifact() = tasks.withType(Upload::class.java).any { task -> task.enabled }