com.gabrielittner.github.diff.GitHubDiffPoster.kt Maven / Gradle / Ivy
Show all versions of github-diff Show documentation
package com.gabrielittner.github.diff
import com.jakewharton.byteunits.BinaryByteUnit
import difflib.ChangeDelta
import difflib.DiffUtils
import difflib.InsertDelta
import difflib.Patch
import org.kohsuke.github.GitHub
import kotlin.math.abs
class GitHubDiffPoster(
private val config: Config,
private val github: GitHub,
private val storage: Storage
) {
fun process(apks: List) {
if (config.prs.isEmpty()) {
println("Storing apk info")
apks.forEach { storage.save(config.commit, it) }
} else {
println("Posting diff")
config.prs.forEach { postApkDiff(it, apks) }
}
}
private fun postApkDiff(pullRequestNumber: Int, apks: List) {
val repository = github.getRepository("${config.userName}/${config.repository}")
val pullRequest = repository.getPullRequest(pullRequestNumber)
val latestRelease = repository.listTags().maxBy { it.commit.commitDate }
val comment = apks.joinToString(separator = "\n\n") { apk ->
val baseCommit = pullRequest.base.sha
val base = storage.loadApk(apk.name, baseCommit)
val releaseCommit = latestRelease?.commit?.shA1
val release = storage.loadApk(apk.name, releaseCommit)
val builder = StringBuilder("### ${apk.name}")
val currentLength = builder.length
if (base != null) {
builder.appendImportantDiff("permissions", apk.permissions, base.permissions)
builder.appendImportantDiff("required features", apk.features.keys, base.features.keys)
builder.appendHighlight("base", "apk size", apk.fileSize, base.fileSize, BASE_APK_SIZE, ::binaryFormat)
builder.appendHighlight("base", "method count", apk.methodCountInt, base.methodCountInt, BASE_METHOD_COUNT, ::simpleFormat)
}
if (release != null) {
builder.appendHighlight("release", "apk size", apk.fileSize, release.fileSize, RELEASE_APK_SIZE, ::binaryFormat)
builder.appendHighlight("release", "method count", apk.methodCountInt, release.methodCountInt, RELEASE_METHOD_COUNT, ::simpleFormat)
}
if (builder.length == currentLength) {
builder.append("\n No major changes.")
}
builder.append("\n\n\nDetails
\n\n\n")
builder.appendTable(apk, base, baseCommit, release, releaseCommit)
builder.append("\n
\n")
if (base != null) {
builder.append("\n\n\nBase diff
\n\n\n")
builder.appendDiff("base" ,"Permissions", apk.permissions, base.permissions)
builder.appendDiff("base" ,"Required features", apk.features.map { it.key + " " + it.value}, base.features.map { it.key + " " + it.value})
builder.appendDiff("base" ,"Not required features", apk.featuresNot, base.featuresNot)
builder.append("\n
\n")
}
if (release != null) {
builder.append("\n\n\nRelease diff
\n\n\n")
builder.appendDiff("release" ,"Permissions", apk.permissions, release.permissions)
builder.appendDiff("release" ,"Required features", apk.features.map { it.key + " " + it.value}, release.features.map { it.key + " " + it.value})
builder.appendDiff("release" ,"Not required features", apk.featuresNot, release.featuresNot)
builder.append("\n
\n")
}
builder.append("\n\n").toString()
}
pullRequest.comment(comment)
}
private fun StringBuilder.appendHighlight(type: String, label: String, value: Int, oldValue: Int, threshold: Int, format: (Int) -> String): StringBuilder {
val sizeDiff = value - oldValue
val percentage = value * 100.0 / oldValue - 100
if (abs(sizeDiff) > threshold) {
append("\n")
if (sizeDiff > 0) {
append(":warning:️ Compared to $type the $label increased by ")
} else {
append(":white_check_mark: Compared to $type the $label decreased by ")
}
append(format(sizeDiff))
append(formatPercentage(percentage))
}
return this
}
private fun StringBuilder.appendImportantDiff(label: String, current: Collection, base: Collection) {
val patch = patch(current, base) ?: return
var added = 0
patch.deltas.forEach {
if (it is InsertDelta || it is ChangeDelta) {
added += it.revised.size()
}
}
if (added > 0) {
append("\n:bangbang: added $added $label")
}
}
private fun StringBuilder.appendTable(apk: Apk, base: Apk?, baseCommit: String, release: Apk?, releaseCommit: String?): StringBuilder {
return append("""
| | current | base | | release | |
| -------- | ------------------ | -------------------- | --------------------------------- | ----------------------------- | ------------------------------------ |
| Commit | ${apk.commit} | ${baseCommit} | | ${releaseCommit ?: "n/a"} | |
| Apk size | ${apk.apkSize} | ${base?.apkSize} | ${apkSizeRelative(apk, base)} | ${release?.apkSize ?: ""} | ${apkSizeRelative(apk, release)} |
| Methods | ${apk.methodCount} | ${base?.methodCount} | ${methodCountRelative(apk, base)} | ${release?.methodCount ?: ""} | ${methodCountRelative(apk, release)} |
| Fields | ${apk.fieldCount} | ${base?.fieldCount} | ${fieldCountRelative(apk, base)} | ${release?.fieldCount ?: ""} | ${fieldCountRelative(apk, release)} |
""".trimIndent()).append("\n\n")
}
private fun StringBuilder.appendDiff(type: String, label: String, current: Collection, base: Collection) {
val diff = diff(type, current, base)
append("\n**$label**\n```diff\n")
if (diff == null) {
append("No changes\n")
} else {
diff.forEach { append(it).append("\n") }
}
append("```")
}
private fun patch(current: Collection, base: Collection): Patch? {
val patch = DiffUtils.diff(base.toList(), current.toList())
if (patch.deltas.isEmpty()) {
return null
}
return patch
}
private fun diff(type: String, current: Collection, base: Collection): List? {
val patch = patch(current, base) ?: return null
return DiffUtils.generateUnifiedDiff(type, "current", base.toList(), patch, 2)
}
private val Apk.apkSize get() = binaryFormat(fileSize)
private val Apk.methodCount get() = simpleFormat(methodCountInt)
private val Apk.fieldCount get() = simpleFormat(fieldCountInt)
private fun apkSizeRelative(apk: Apk, old: Apk?) = formatRelative(apk.fileSize, old?.fileSize, ::binaryFormat)
private fun methodCountRelative(apk: Apk, old: Apk?) = formatRelative(apk.methodCountInt, old?.methodCountInt, ::simpleFormat)
private fun fieldCountRelative(apk: Apk, old: Apk?) = formatRelative(apk.fieldCountInt, old?.fieldCountInt, ::simpleFormat)
private fun formatRelative(value: Int, oldValue: Int?, formatDiff: (Int) -> String): String {
if (oldValue == null || oldValue == 0) {
return ""
}
val diff = value - oldValue
val percentage = value * 100.0 / oldValue - 100
val prefix = when {
diff < 0 -> "-"
diff > 0 -> "+"
else -> ""
}
val formatted = "$prefix${formatDiff(diff)}${formatPercentage(percentage)}"
if (abs(percentage) >= 5.0) {
return "**$formatted**"
}
return formatted
}
private fun simpleFormat(count: Int) = String.format("%,d", abs(count))
private fun binaryFormat(size: Int) = BinaryByteUnit.format(abs(size).toLong(), "#.##")!!
private fun formatPercentage(percentage: Double): String {
if (abs(percentage) < 0.1) {
return ""
}
return String.format(" (%+.1f%%)", percentage)
}
private companion object {
private const val BASE_APK_SIZE = 250 * 1024
private const val RELEASE_APK_SIZE = 1024 * 1024
private const val BASE_METHOD_COUNT = 500
private const val RELEASE_METHOD_COUNT = 5000
}
}