diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c8ddcf4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.{kt,kts,gradle}] +indent_style = space +indent_size = 2 +ij_kotlin_name_count_to_use_star_import = 2147483647 +ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 +ij_kotlin_packages_to_use_import_on_demand = + + + diff --git a/.gitignore b/.gitignore index d57aba4..bfe6ae8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ build /.gradle /.idea +!.idea/fileTemplates/ diff --git a/.idea/fileTemplates/includes/File Header.java b/.idea/fileTemplates/includes/File Header.java new file mode 100644 index 0000000..48ab8ea --- /dev/null +++ b/.idea/fileTemplates/includes/File Header.java @@ -0,0 +1,15 @@ +/* +* Copyright Careem, an Uber Technologies Inc. company +* +* 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. +*/ diff --git a/README.md b/README.md index a4bc55d..5ce3618 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ upgrades (ex upgrading Firebase here also updates it in ...) passing a collapse of `com.careem.care` would result in collapsing updates of `com.careem.care:dep1` and `com.careem.care:dep2` to just `com.careem.care:*`. This flag is repeatable. +* `-o, --output` - output type, you can pass `plain` or `json`, `plain` is the default argument ## Other Utilities diff --git a/src/main/kotlin/com/careem/gradle/dependencies/effectFinder.kt b/src/main/kotlin/com/careem/gradle/dependencies/effectFinder.kt index 965fc3a..0f32a44 100644 --- a/src/main/kotlin/com/careem/gradle/dependencies/effectFinder.kt +++ b/src/main/kotlin/com/careem/gradle/dependencies/effectFinder.kt @@ -65,7 +65,7 @@ fun upgradeEffects(old: String, new: String, collapseKeys: List): String if (index > 0) { appendLine() } - append("Changing ${effects.first.artifact} to ${effects.first.version} will also change:") + append("Changing ${effects.first.artifact} to ${effects.first.version}${effects.first.alternativeVersion?.let { ", (changed from $it)"}} will also change:") effects.second .forEach { affectedDependency -> writeTree(affectedDependency, 1) } appendLine() diff --git a/src/main/kotlin/com/careem/gradle/dependencies/main.kt b/src/main/kotlin/com/careem/gradle/dependencies/main.kt index 9a67fe4..e6e7468 100644 --- a/src/main/kotlin/com/careem/gradle/dependencies/main.kt +++ b/src/main/kotlin/com/careem/gradle/dependencies/main.kt @@ -20,6 +20,7 @@ package com.careem.gradle.dependencies import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.options.default import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.multiple import com.github.ajalt.clikt.parameters.options.option @@ -38,6 +39,11 @@ class Tldr : CliktCommand(name = "dependency-diff-tldr") { help = "Collapse packages with a matching group under a group.*. Collapsing " + "will only occur if all version numbers match. (ex --collapse com.careem.ridehail --collapse com.careem.now)." ).multiple() + private val output by option( + "-o", + "--output", + help = "Output type, \"plain\" and \"json\" are supported" + ).default("plain") private val old by argument("old.txt").path(mustExist = true) private val new by argument("new.txt").path(mustExist = true) @@ -45,7 +51,7 @@ class Tldr : CliktCommand(name = "dependency-diff-tldr") { override fun run() { val oldContents = old.readText() val newContents = new.readText() - print(tldr(oldContents, newContents, collapse)) + print(tldr(oldContents, newContents).toString(collapse, outputType = output.toOutputType())) if (sideEffects) { val upgradeEffects = upgradeEffects(oldContents, newContents, collapse) if (upgradeEffects.isNotEmpty()) { diff --git a/src/main/kotlin/com/careem/gradle/dependencies/mismatched/main.kt b/src/main/kotlin/com/careem/gradle/dependencies/mismatched/main.kt index 719b133..8bd13c5 100644 --- a/src/main/kotlin/com/careem/gradle/dependencies/mismatched/main.kt +++ b/src/main/kotlin/com/careem/gradle/dependencies/mismatched/main.kt @@ -1,3 +1,19 @@ +/* +* Copyright Careem, an Uber Technologies Inc. company +* +* 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. +*/ + @file:JvmName("MismatchedVersionFinder") package com.careem.gradle.dependencies.mismatched diff --git a/src/main/kotlin/com/careem/gradle/dependencies/printer.kt b/src/main/kotlin/com/careem/gradle/dependencies/printer.kt new file mode 100644 index 0000000..7ac114b --- /dev/null +++ b/src/main/kotlin/com/careem/gradle/dependencies/printer.kt @@ -0,0 +1,114 @@ +/* +* Copyright Careem, an Uber Technologies Inc. company +* +* 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 com.careem.gradle.dependencies +enum class OutputType{ + PLAIN, JSON +} + +fun String.toOutputType(): OutputType = when(this){ + "json" -> OutputType.JSON + else -> OutputType.PLAIN +} + + +fun VersionDifferences.toString(collapse: List, outputType: OutputType): String { + return when(outputType){ + OutputType.PLAIN -> writePlain(this, collapse) + OutputType.JSON -> writeJson(this, collapse) + } +} + +private fun writePlain(versionDifferences: VersionDifferences, collapse: List): String = buildString { + val (added, removed, upgraded) = versionDifferences + writeList("New Dependencies", added) + writeList("Removed Dependencies", removed, added.isNotEmpty()) + writeList("Upgraded Dependencies", collapseDependencies(upgraded, collapse), + removed.isNotEmpty() || added.isNotEmpty()) +} + +private fun writeJson(versionDifferences: VersionDifferences, collapse: List): String = buildString { + val (added, removed, upgraded) = versionDifferences + append("{") + writeJsonObject("new_dependencies", added, trailingComma = true) + writeJsonObject("removed_dependencies", removed, trailingComma = true) + writeJsonObject("upgraded_dependencies", collapseDependencies(upgraded, collapse)) + append("}") +} + +private fun StringBuilder.writeList(title: String, list: List, newLineBeforeTitle: Boolean = false) { + if (list.isNotEmpty()) { + if (newLineBeforeTitle) { + append("\n") + } + append(title) + append("\n") + list.forEach { + append(it.artifact) + if (it.version.isNotBlank()) { + append(":") + append(it.version) + it.alternativeVersion?.let { altVersion -> append(", (changed from $altVersion)") } + } + append("\n") + } + } +} + +private fun StringBuilder.writeJsonObject(key: String, list: List, trailingComma: Boolean = false) { + append("\"$key\":[") + if (list.isNotEmpty()) { + list.forEachIndexed { index, versionedDependency -> + append(writeJsonVersionedDependency(versionedDependency)) + if(index + 1 < list.size){ + append(",") + } + } + } + append("]") + if(trailingComma){ + append(",") + } +} + +private fun writeJsonVersionedDependency(versionedDependency: VersionedDependency): String { + return "{\"artifact\":\"${versionedDependency.artifact}\",\"version\":\"${versionedDependency.version}\",\"other_version\":\"${versionedDependency.alternativeVersion}\"}" +} + + +private fun collapseDependencies( + dependencies: List, + collapses: List +): List { + val add = mutableSetOf() + val remove = mutableSetOf() + + collapses.forEach { collapse -> + val matchingToCollapse = dependencies.filter { it.artifact.startsWith(collapse) } + val versions = matchingToCollapse.map { it.version }.toSet().size + if (versions == 1) { + remove.addAll(matchingToCollapse) + add.add(matchingToCollapse[0].copy(artifact = collapse)) + } + } + + return if (add.isNotEmpty()) { + (dependencies - remove) + add + } else { + dependencies + } +} + diff --git a/src/main/kotlin/com/careem/gradle/dependencies/treeTldr.kt b/src/main/kotlin/com/careem/gradle/dependencies/treeTldr.kt index d5f0e9e..96c72e5 100644 --- a/src/main/kotlin/com/careem/gradle/dependencies/treeTldr.kt +++ b/src/main/kotlin/com/careem/gradle/dependencies/treeTldr.kt @@ -16,15 +16,8 @@ package com.careem.gradle.dependencies -fun tldr(old: String, new: String, collapse: List): String { - val (added, removed, upgraded) = dependencyDifferences(old, new) - - return buildString { - writeList("New Dependencies", added) - writeList("Removed Dependencies", removed, added.isNotEmpty()) - writeList("Upgraded Dependencies", collapseDependencies(upgraded, collapse), - removed.isNotEmpty() || added.isNotEmpty()) - } +fun tldr(old: String, new: String): VersionDifferences { + return dependencyDifferences(old, new) } fun dependencyDifferences(old: String, new: String): VersionDifferences { @@ -68,30 +61,7 @@ private fun extractDependencies(deps: String): Set { .toSet() } -private fun collapseDependencies( - dependencies: List, - collapses: List -): List { - val add = mutableSetOf() - val remove = mutableSetOf() - - collapses.forEach { collapse -> - val matchingToCollapse = dependencies.filter { it.artifact.startsWith(collapse) } - val versions = matchingToCollapse.map { it.version }.toSet().size - if (versions == 1) { - remove.addAll(matchingToCollapse) - add.add(matchingToCollapse[0].copy(artifact = collapse)) - } - } - - return if (add.isNotEmpty()) { - (dependencies - remove) + add - } else { - dependencies - } -} - -data class VersionedDependency(val artifact: String, val version: String) { +data class VersionedDependency(val artifact: String, val version: String, val alternativeVersion: String? = null) { val group by lazy { artifact.substringBefore(':').trim() } } data class VersionDifferences( @@ -115,7 +85,7 @@ private fun partitionDifferences( removedVersion != null -> { mutableRemovedMap.remove(artifact) upgrades.add( - VersionedDependency(artifact, "$version, (changed from $removedVersion)")) + VersionedDependency(artifact, version, alternativeVersion = removedVersion)) } else -> { additions.add(VersionedDependency(artifact, version)) @@ -133,20 +103,4 @@ private fun partitionDifferences( ) } -private fun StringBuilder.writeList(title: String, list: List, newLineBeforeTitle: Boolean = false) { - if (list.isNotEmpty()) { - if (newLineBeforeTitle) { - append("\n") - } - append(title) - append("\n") - list.forEach { - append(it.artifact) - if (it.version.isNotBlank()) { - append(":") - append(it.version) - } - append("\n") - } - } -} \ No newline at end of file + diff --git a/src/test/fixtures/local_project_update/expected_json.json b/src/test/fixtures/local_project_update/expected_json.json new file mode 100644 index 0000000..6865aa5 --- /dev/null +++ b/src/test/fixtures/local_project_update/expected_json.json @@ -0,0 +1 @@ +{"new_dependencies":[],"removed_dependencies":[],"upgraded_dependencies":[{"artifact":"androidx.activity:activity","version":"1.5.1","other_version":"1.2.4"},{"artifact":"androidx.annotation:annotation-experimental","version":"1.3.0","other_version":"1.1.0"},{"artifact":"androidx.appcompat:appcompat","version":"1.5.1","other_version":"1.4.1"},{"artifact":"androidx.appcompat:appcompat-resources","version":"1.5.1","other_version":"1.4.1"},{"artifact":"androidx.core:core","version":"1.9.0","other_version":"1.7.0"},{"artifact":"androidx.core:core-ktx","version":"1.9.0","other_version":"1.7.0"},{"artifact":"androidx.databinding:viewbinding","version":"7.3.0","other_version":"7.1.2"},{"artifact":"androidx.emoji2:emoji2","version":"1.2.0","other_version":"1.0.0"},{"artifact":"androidx.emoji2:emoji2-views-helper","version":"1.2.0","other_version":"1.0.0"},{"artifact":"androidx.lifecycle:lifecycle-common","version":"2.5.1","other_version":"2.4.0"},{"artifact":"androidx.lifecycle:lifecycle-livedata-core","version":"2.5.1","other_version":"2.3.1"},{"artifact":"androidx.lifecycle:lifecycle-process","version":"2.4.1","other_version":"2.4.0"},{"artifact":"androidx.lifecycle:lifecycle-runtime","version":"2.5.1","other_version":"2.4.0"},{"artifact":"androidx.lifecycle:lifecycle-viewmodel","version":"2.5.1","other_version":"2.3.1"},{"artifact":"androidx.lifecycle:lifecycle-viewmodel-savedstate","version":"2.5.1","other_version":"2.3.1"},{"artifact":"androidx.resourceinspection:resourceinspection-annotation","version":"1.0.1","other_version":"1.0.0"},{"artifact":"androidx.savedstate:savedstate","version":"1.2.0","other_version":"1.1.0"},{"artifact":"androidx.startup:startup-runtime","version":"1.1.1","other_version":"1.0.0"},{"artifact":"com.google.android.material:material","version":"1.6.1","other_version":"1.5.0"},{"artifact":"com.squareup.okhttp3:okhttp","version":"4.10.0","other_version":"4.9.3"},{"artifact":"com.squareup.okhttp3:okhttp-dnsoverhttps","version":"4.10.0","other_version":"4.9.3"},{"artifact":"com.squareup.okio:okio","version":"3.2.0","other_version":"3.0.0"},{"artifact":"com.squareup.okio:okio-jvm","version":"3.2.0","other_version":"3.0.0"},{"artifact":"org.jetbrains.kotlin:kotlin-stdlib","version":"1.7.10","other_version":"1.6.10"},{"artifact":"org.jetbrains.kotlin:kotlin-stdlib-common","version":"1.7.10","other_version":"1.6.10"},{"artifact":"org.jetbrains.kotlin:kotlin-stdlib-jdk7","version":"1.7.10","other_version":"1.6.10"},{"artifact":"org.jetbrains.kotlin:kotlin-stdlib-jdk8","version":"1.7.10","other_version":"1.6.10"},{"artifact":"org.jetbrains.kotlinx:kotlinx-coroutines-android","version":"1.6.4","other_version":"1.6.0"},{"artifact":"org.jetbrains.kotlinx:kotlinx-coroutines-bom","version":"1.6.4","other_version":"1.6.0"},{"artifact":"org.jetbrains.kotlinx:kotlinx-coroutines-core","version":"1.6.4","other_version":"1.6.0"},{"artifact":"org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm","version":"1.6.4","other_version":"1.6.0"}]} \ No newline at end of file diff --git a/src/test/fixtures/noop_update/expected_json.json b/src/test/fixtures/noop_update/expected_json.json new file mode 100644 index 0000000..f9f9b1f --- /dev/null +++ b/src/test/fixtures/noop_update/expected_json.json @@ -0,0 +1 @@ +{"new_dependencies":[],"removed_dependencies":[],"upgraded_dependencies":[]} \ No newline at end of file diff --git a/src/test/fixtures/project_update/expected_json.json b/src/test/fixtures/project_update/expected_json.json new file mode 100644 index 0000000..477719e --- /dev/null +++ b/src/test/fixtures/project_update/expected_json.json @@ -0,0 +1 @@ +{"new_dependencies":[{"artifact":"androidx.concurrent:concurrent-futures","version":"1.0.0","other_version":"null"},{"artifact":"androidx.constraintlayout:constraintlayout","version":"2.0.1","other_version":"null"},{"artifact":"androidx.constraintlayout:constraintlayout-solver","version":"2.0.1","other_version":"null"},{"artifact":"androidx.documentfile:documentfile","version":"1.0.0","other_version":"null"},{"artifact":"androidx.dynamicanimation:dynamicanimation","version":"1.0.0","other_version":"null"},{"artifact":"androidx.emoji2:emoji2","version":"1.0.0","other_version":"null"},{"artifact":"androidx.emoji2:emoji2-views-helper","version":"1.0.0","other_version":"null"},{"artifact":"androidx.legacy:legacy-support-core-utils","version":"1.0.0","other_version":"null"},{"artifact":"androidx.lifecycle:lifecycle-process","version":"2.4.0","other_version":"null"},{"artifact":"androidx.lifecycle:lifecycle-viewmodel-savedstate","version":"2.3.1","other_version":"null"},{"artifact":"androidx.localbroadcastmanager:localbroadcastmanager","version":"1.0.0","other_version":"null"},{"artifact":"androidx.print:print","version":"1.0.0","other_version":"null"},{"artifact":"androidx.resourceinspection:resourceinspection-annotation","version":"1.0.0","other_version":"null"},{"artifact":"androidx.startup:startup-runtime","version":"1.0.0","other_version":"null"},{"artifact":"androidx.tracing:tracing","version":"1.0.0","other_version":"null"},{"artifact":"com.google.guava:listenablefuture","version":"1.0","other_version":"null"},{"artifact":"com.squareup.okio:okio-jvm","version":"3.0.0","other_version":"null"},{"artifact":"org.jetbrains.kotlinx:kotlinx-coroutines-bom","version":"1.6.0","other_version":"null"}],"removed_dependencies":[],"upgraded_dependencies":[{"artifact":"androidx.activity:activity","version":"1.2.4","other_version":"1.0.0"},{"artifact":"androidx.annotation:annotation","version":"1.3.0","other_version":"1.1.0"},{"artifact":"androidx.annotation:annotation-experimental","version":"1.1.0","other_version":"1.0.0"},{"artifact":"androidx.appcompat:appcompat","version":"1.4.1","other_version":"1.2.0"},{"artifact":"androidx.appcompat:appcompat-resources","version":"1.4.1","other_version":"1.2.0"},{"artifact":"androidx.arch.core:core-runtime","version":"2.1.0","other_version":"2.0.0"},{"artifact":"androidx.core:core","version":"1.7.0","other_version":"1.3.2"},{"artifact":"androidx.core:core-ktx","version":"1.7.0","other_version":"1.3.2"},{"artifact":"androidx.customview:customview","version":"1.1.0","other_version":"1.0.0"},{"artifact":"androidx.databinding:viewbinding","version":"7.1.2","other_version":"7.0.0-alpha03"},{"artifact":"androidx.drawerlayout:drawerlayout","version":"1.1.1","other_version":"1.0.0"},{"artifact":"androidx.fragment:fragment","version":"1.3.6","other_version":"1.1.0"},{"artifact":"androidx.lifecycle:lifecycle-common","version":"2.4.0","other_version":"2.1.0"},{"artifact":"androidx.lifecycle:lifecycle-livedata-core","version":"2.3.1","other_version":"2.0.0"},{"artifact":"androidx.lifecycle:lifecycle-runtime","version":"2.4.0","other_version":"2.1.0"},{"artifact":"androidx.lifecycle:lifecycle-viewmodel","version":"2.3.1","other_version":"2.1.0"},{"artifact":"androidx.savedstate:savedstate","version":"1.1.0","other_version":"1.0.0"},{"artifact":"androidx.versionedparcelable:versionedparcelable","version":"1.1.1","other_version":"1.1.0"},{"artifact":"com.google.android.material:material","version":"1.5.0","other_version":"1.2.1"},{"artifact":"com.jakewharton.timber:timber","version":"5.0.1","other_version":"4.7.1"},{"artifact":"com.squareup.okhttp3:okhttp","version":"4.9.3","other_version":"4.9.0"},{"artifact":"com.squareup.okhttp3:okhttp-dnsoverhttps","version":"4.9.3","other_version":"4.9.0"},{"artifact":"com.squareup.okio:okio","version":"3.0.0","other_version":"2.9.0"},{"artifact":"org.jetbrains.kotlin:kotlin-stdlib","version":"1.6.10","other_version":"1.4.21"},{"artifact":"org.jetbrains.kotlin:kotlin-stdlib-common","version":"1.6.10","other_version":"1.4.21"},{"artifact":"org.jetbrains.kotlin:kotlin-stdlib-jdk7","version":"1.6.10","other_version":"1.4.10"},{"artifact":"org.jetbrains.kotlin:kotlin-stdlib-jdk8","version":"1.6.10","other_version":"1.4.10"},{"artifact":"org.jetbrains.kotlinx:kotlinx-coroutines-android","version":"1.6.0","other_version":"1.4.2"},{"artifact":"org.jetbrains.kotlinx:kotlinx-coroutines-core","version":"1.6.0","other_version":"1.4.2"},{"artifact":"org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm","version":"1.6.0","other_version":"1.4.2"},{"artifact":"org.jetbrains:annotations","version":"20.1.0","other_version":"16.0.1"}]} \ No newline at end of file diff --git a/src/test/kotlin/com/careem/gradle/dependencies/FixturesTest.kt b/src/test/kotlin/com/careem/gradle/dependencies/FixturesTest.kt index d5ac1e1..2ae3ff7 100644 --- a/src/test/kotlin/com/careem/gradle/dependencies/FixturesTest.kt +++ b/src/test/kotlin/com/careem/gradle/dependencies/FixturesTest.kt @@ -1,3 +1,19 @@ +/* +* Copyright Careem, an Uber Technologies Inc. company +* +* 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 com.careem.gradle.dependencies import com.google.common.truth.Truth.assertThat @@ -15,7 +31,16 @@ class FixturesTest(private val fixtureDir: File) { val before = fixtureDir.resolve("before.txt").readText() val after = fixtureDir.resolve("after.txt").readText() val expected = fixtureDir.resolve("expected.txt").readText() - val actual = tldr(before, after, emptyList()) + val actual = tldr(before, after).toString(collapse = emptyList(), outputType = OutputType.PLAIN) + assertThat(actual).isEqualTo(expected) + } + + @Test + fun testTldrJson() { + val before = fixtureDir.resolve("before.txt").readText() + val after = fixtureDir.resolve("after.txt").readText() + val expected = fixtureDir.resolve("expected_json.json").readText() + val actual = tldr(before, after).toString(collapse = emptyList(), outputType = OutputType.JSON) assertThat(actual).isEqualTo(expected) }