All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.gabrielittner.github.diff.GitHubDiffPoster.kt Maven / Gradle / Ivy

There is a newer version: 0.8.0
Show newest version
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 } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy