diff --git a/src/main/kotlin/me/msoucy/gbat/Analyze.kt b/src/main/kotlin/me/msoucy/gbat/Analyze.kt index eb15141..bfe611a 100644 --- a/src/main/kotlin/me/msoucy/gbat/Analyze.kt +++ b/src/main/kotlin/me/msoucy/gbat/Analyze.kt @@ -1,21 +1,20 @@ package me.msoucy.gbat import java.io.File -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.transactions.transaction - -import me.msoucy.gbat.models.CondensedAnalysis import me.msoucy.gbat.models.Condensation +import me.msoucy.gbat.models.CondensedAnalysis import me.msoucy.gbat.models.KnowledgeModel import me.msoucy.gbat.models.LineModel import me.msoucy.gbat.models.RiskModel +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction fun analyze( - riskModel : RiskModel, - createdConstant : Double, - historyItem : HistoryItem, - verbose : Boolean = false -) : CondensedAnalysis { + riskModel: RiskModel, + createdConstant: Double, + historyItem: HistoryItem, + verbose: Boolean = false +): CondensedAnalysis { val lineModel = LineModel() val db = Database.connect("jdbc:sqlite::memory:", "org.sqlite.JDBC") return transaction(db) { @@ -25,8 +24,8 @@ fun analyze( historyItem.authorDiffs.forEach { (author, changes) -> changes.forEach { change -> changesProcessed++ - if(changesProcessed % 1000 == 0 && verbose) { - System.err.println("Analyzer applied change #${changesProcessed}") + if (changesProcessed % 1000 == 0 && verbose) { + System.err.println("Analyzer applied change #$changesProcessed") } lineModel.apply(change.eventType, change.lineNum, change.lineVal ?: "") knowledgeModel.apply(change.eventType, author, change.lineNum) @@ -44,21 +43,21 @@ fun analyze( } private fun condenseAnalysis( - repoRoot : File, - projectRoot : File, - fname : File, - lineModel : LineModel, - knowledgeModel : KnowledgeModel, - riskModel : RiskModel -) : CondensedAnalysis { + repoRoot: File, + projectRoot: File, + fname: File, + lineModel: LineModel, + knowledgeModel: KnowledgeModel, + riskModel: RiskModel +): CondensedAnalysis { val condensations = lineModel.get().mapIndexed { idx, line -> val knowledges = knowledgeModel.knowledgeSummary(idx + 1).map { (authors, knowledge) -> Condensation(authors, knowledge, - if(authors.all(riskModel::isDeparted)) knowledge else 0.0, + if (authors.all(riskModel::isDeparted)) knowledge else 0.0, riskModel.jointBusProb(authors) * knowledge) }.sorted() Pair(line, knowledges) } return CondensedAnalysis(repoRoot, projectRoot, fname, condensations.mutableCopyOf()) -} \ No newline at end of file +} diff --git a/src/main/kotlin/me/msoucy/gbat/Main.kt b/src/main/kotlin/me/msoucy/gbat/Main.kt index 80ecf43..29ca314 100644 --- a/src/main/kotlin/me/msoucy/gbat/Main.kt +++ b/src/main/kotlin/me/msoucy/gbat/Main.kt @@ -1,10 +1,10 @@ package me.msoucy.gbat -import me.msoucy.gbat.models.RiskModel -import me.msoucy.gbat.models.SummaryModel - +import com.xenomachina.argparser.ArgParser +import com.xenomachina.argparser.InvalidArgumentException +import com.xenomachina.argparser.default +import com.xenomachina.argparser.mainBody import java.io.File -import java.util.concurrent.Executors import kotlin.math.pow import kotlin.system.exitProcess import kotlin.text.Regex @@ -12,90 +12,85 @@ import kotlin.text.RegexOption import kotlin.text.startsWith import kotlinx.coroutines.* import kotlinx.coroutines.flow.* - -import com.xenomachina.argparser.ArgParser -import com.xenomachina.argparser.InvalidArgumentException -import com.xenomachina.argparser.default -import com.xenomachina.argparser.mainBody - +import me.msoucy.gbat.models.RiskModel +import me.msoucy.gbat.models.SummaryModel import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.transactions.transaction val REALLY_LONG_TIME = 864000 val DEFAULT_INTERESTING_RES = mutableListOf( - "\\.java$", - "\\.cs$", - "\\.py$", - "\\.c$", - "\\.cpp$", - "\\.h$", - "\\.hpp$", - "\\.pl$", - "\\.perl$", - "\\.rb$", - "\\.sh$", + "\\.java$", + "\\.cs$", + "\\.py$", + "\\.c$", + "\\.cpp$", + "\\.h$", + "\\.hpp$", + "\\.pl$", + "\\.perl$", + "\\.rb$", + "\\.sh$", "\\.js$", "\\.kt$" ) -fun validateGit(exe : String) : String { +fun validateGit(exe: String): String { val os = System.getProperty("os.name") - var fullexe = if(os.startsWith("Windows") && !exe.endsWith(".exe")) { + var fullexe = if (os.startsWith("Windows") && !exe.endsWith(".exe")) { exe + ".exe" } else exe var file = File(fullexe) - if(file.canRead()) { + if (file.canRead()) { return file.absolutePath } - for(path in System.getenv("PATH").split(";")) { + for (path in System.getenv("PATH").split(";")) { file = File(path, fullexe) - if(file.canRead()) { + if (file.canRead()) { return file.absolutePath } } - throw InvalidArgumentException("Provided git executable does not exist") + throw InvalidArgumentException("Provided git executable does not exist") } class GbatArgs(parser: ArgParser) { - // Input options - val interesting by parser.adding("--interesting", "-I", - help="Regular expression to determine which files should be included in calculations.") - val not_interesting by parser.adding("--not-interesting", "-N", - help="Regular expression to determine which files should not be included in calculations.") - val case_sensitive by parser.flagging("Use case sensitive regexps when determining interesting files (default is case-insensitive)") - val departed by parser.storing("--departed-file", "-D", help="File listing departed devs, one per line", transform=::File).default(null) - val risk_file by parser.storing("--bus-risk-file", help="File of dev=float lines (e.g. ejorgensen=0.4) with custom bus risks for devs", transform=::File).default(null) - val default_bus_risk by parser.storing("--default-bus-risk", help="Default risk that a dev will be hit by a bus in your analysis timeframe (defaults to 0.1).") { toDouble() }.default(0.1) + // Input options + val interesting by parser.adding("--interesting", "-I", + help = "Regular expression to determine which files should be included in calculations.") + val not_interesting by parser.adding("--not-interesting", "-N", + help = "Regular expression to determine which files should not be included in calculations.") + val case_sensitive by parser.flagging("Use case sensitive regexps when determining interesting files (default is case-insensitive)") + val departed by parser.storing("--departed-file", "-D", help = "File listing departed devs, one per line", transform = ::File).default(null) + val risk_file by parser.storing("--bus-risk-file", help = "File of dev=float lines (e.g. ejorgensen=0.4) with custom bus risks for devs", transform = ::File).default(null) + val default_bus_risk by parser.storing("--default-bus-risk", help = "Default risk that a dev will be hit by a bus in your analysis timeframe (defaults to 0.1).") { toDouble() }.default(0.1) - // Multiprocessing options - val num_git_procs by parser.storing("--num-git-procs", help="The number of git processes to run simultaneously (defaults to 3)") { toInt() }.default(3) - val num_analyzer_procs by parser.storing("--num-analyzer-procs", help="The number of analyzer processes to run (defaults to 3)") { toInt() }.default(3) + // Multiprocessing options + val num_git_procs by parser.storing("--num-git-procs", help = "The number of git processes to run simultaneously (defaults to 3)") { toInt() }.default(3) + val num_analyzer_procs by parser.storing("--num-analyzer-procs", help = "The number of analyzer processes to run (defaults to 3)") { toInt() }.default(3) - // Tuning options - val risk_threshold by parser.storing("--risk-threshold", help="Threshold past which to summarize risk (defaults to default bus risk cubed)") { toDouble() }.default(null) - val creation_constant by parser.storing("--knowledge-creation-constant", help="How much knowledge a changed line should create if a new line creates 1 (defaults to 0.1)") { toDouble() }.default(0.1) + // Tuning options + val risk_threshold by parser.storing("--risk-threshold", help = "Threshold past which to summarize risk (defaults to default bus risk cubed)") { toDouble() }.default(null) + val creation_constant by parser.storing("--knowledge-creation-constant", help = "How much knowledge a changed line should create if a new line creates 1 (defaults to 0.1)") { toDouble() }.default(0.1) - // Misc options - val git_exe by parser.storing("--git-exe", help="Path to the git executable", transform=::validateGit).default("git").addValidator { validateGit(value) } - val verbose by parser.flagging("--verbose", "-v", help="Print comforting output") - val output by parser.storing("Output directory for data files and html summary (defaults to \"output\"), error if already exists").default("output") + // Misc options + val git_exe by parser.storing("--git-exe", help = "Path to the git executable", transform = ::validateGit).default("git").addValidator { validateGit(value) } + val verbose by parser.flagging("--verbose", "-v", help = "Print comforting output") + val output by parser.storing("Output directory for data files and html summary (defaults to \"output\"), error if already exists").default("output") - // Directory + // Directory val project_root by parser.positional("The root directory to inspect") } fun main(args: Array) = mainBody { - ArgParser(args).parseInto(::GbatArgs).run { + ArgParser(args).parseInto(::GbatArgs).run { val outDir = File(output) - if(outDir.isDirectory) { - //throw InvalidArgumentException("Output directory already exists") + if (outDir.isDirectory) { + // throw InvalidArgumentException("Output directory already exists") } outDir.mkdirs() - fun parse_interesting(theList : List) = + fun parse_interesting(theList: List) = theList.map { - if(case_sensitive) { + if (case_sensitive) { Regex(it) } else { Regex(it, RegexOption.IGNORE_CASE) @@ -107,21 +102,21 @@ fun main(args: Array) = mainBody { val not_interesting_res = if (not_interesting.isEmpty()) listOf() else parse_interesting(not_interesting) val projectRootFile = File(project_root).also { - if(!it.isDirectory) + if (!it.isDirectory) throw InvalidArgumentException("Provided project root does not exist") } val repo = GitRepo(projectRootFile, validateGit(git_exe)) - fun String.isInteresting() : Boolean { + fun String.isInteresting(): Boolean { var hasInterest = interesting_res.any { it.containsMatchIn(this) } - if(hasInterest) { + if (hasInterest) { hasInterest = !not_interesting_res.any { it.containsMatchIn(this) } } return hasInterest } - fun GitRepo.interestingNames() = ls().split("\n").filter{ it.isInteresting() } + fun GitRepo.interestingNames() = ls().split("\n").filter { it.isInteresting() } val fnames = repo.interestingNames() if (fnames.isEmpty()) { @@ -136,13 +131,8 @@ fun main(args: Array) = mainBody { val riskModel = RiskModel(riskThresh, default_bus_risk, risk_file, departed) val dbFname = File(outDir, "summary.db") - dbFname.delete(); - val summaryDb = Database.connect("jdbc:sqlite:${dbFname.absolutePath}", driver="org.sqlite.JDBC") - transaction(summaryDb) { - addLogger(StdOutSqlLogger) - // exec("PRAGMA journal_mode = OFF") - // exec("PRAGMA synchronous = OFF") - } + dbFname.delete() + val summaryDb = Database.connect("jdbc:sqlite:${dbFname.absolutePath}", driver = "org.sqlite.JDBC") val summaryModel = SummaryModel(summaryDb) runBlocking { @@ -160,6 +150,6 @@ fun main(args: Array) = mainBody { renderSummary(projectRootFile, summaryModel, outDir) // Render summary - System.err.println("Done, summary is in ${outDir}/index.html") + System.err.println("Done, summary is in $outDir/index.html") } } diff --git a/src/main/kotlin/me/msoucy/gbat/ParseHistory.kt b/src/main/kotlin/me/msoucy/gbat/ParseHistory.kt index 730ddbd..21739de 100644 --- a/src/main/kotlin/me/msoucy/gbat/ParseHistory.kt +++ b/src/main/kotlin/me/msoucy/gbat/ParseHistory.kt @@ -3,25 +3,26 @@ package me.msoucy.gbat import java.io.File import kotlin.math.abs import kotlin.math.max - import me.msoucy.gbat.models.ChangeType import me.msoucy.gbat.models.Event data class HistoryItem( - val repoRoot : File, - val projectRoot : File, - val fname : File, - val authorDiffs : List>> + val repoRoot: File, + val projectRoot: File, + val fname: File, + val authorDiffs: List>> ) -fun parseHistory(repo : GitRepo, - projectRoot : File, - fname : File, - verbose : Boolean = false) : HistoryItem { +fun parseHistory( + repo: GitRepo, + projectRoot: File, + fname: File, + verbose: Boolean = false +): HistoryItem { val entries = repo.log(fname) val repoRoot = repo.root() - if(verbose) { - System.err.println("Parsing history for ${fname}") + if (verbose) { + System.err.println("Parsing history for $fname") } return HistoryItem(repoRoot, projectRoot, fname, entries.map { (author, diff) -> @@ -30,27 +31,27 @@ fun parseHistory(repo : GitRepo, ) } -fun diffWalk(diff : Diff) : List { +fun diffWalk(diff: Diff): List { fun String.startsChunk() = startsWith("@@") fun String.isOldLine() = startsWith("-") fun String.isNewLine() = startsWith("+") - fun chunkify() : List> { + fun chunkify(): List> { val chunks = mutableListOf>() var curChunk = mutableListOf() diff.split("\n").forEach { line -> - if(line.startsChunk()) { - if(curChunk.isNotEmpty()) { + if (line.startsChunk()) { + if (curChunk.isNotEmpty()) { chunks.add(curChunk) curChunk = mutableListOf() } curChunk.add(line) - } else if(curChunk.isNotEmpty()) { + } else if (curChunk.isNotEmpty()) { curChunk.add(line) } } - if(curChunk.isNotEmpty()) { + if (curChunk.isNotEmpty()) { chunks.add(curChunk) } return chunks @@ -60,23 +61,23 @@ fun diffWalk(diff : Diff) : List { val events = mutableListOf() class Hunk( - val lineNum : Int, - val oldLines : List, - val newLines : List + val lineNum: Int, + val oldLines: List, + val newLines: List ) - fun hunkize(chunkWoHeader : List, firstLineNum : Int) : List { + fun hunkize(chunkWoHeader: List, firstLineNum: Int): List { var curOld = mutableListOf() var curNew = mutableListOf() var curLine = firstLineNum var hunks = mutableListOf() chunkWoHeader.forEach { line -> - if(line.isOldLine()) { + if (line.isOldLine()) { curOld.add(line) - } else if(line.isNewLine()) { + } else if (line.isNewLine()) { curNew.add(line) - } else if(curOld.isNotEmpty() || curNew.isNotEmpty()) { + } else if (curOld.isNotEmpty() || curNew.isNotEmpty()) { hunks.add(Hunk(curLine, curOld, curNew)) curLine += curNew.size + 1 curOld = mutableListOf() @@ -85,28 +86,28 @@ fun diffWalk(diff : Diff) : List { curLine++ } } - if(curOld.isNotEmpty() || curNew.isNotEmpty()) { + if (curOld.isNotEmpty() || curNew.isNotEmpty()) { hunks.add(Hunk(curLine, curOld, curNew)) } return hunks } - fun stepHunk(hunk : Hunk) { + fun stepHunk(hunk: Hunk) { val oldLen = hunk.oldLines.size val newLen = hunk.newLines.size val maxLen = max(oldLen, newLen) var lineNum = hunk.lineNum for (i in 0 until maxLen) { - if(i < oldLen && i < newLen) { + if (i < oldLen && i < newLen) { events += Event( ChangeType.Change, lineNum, hunk.newLines[i].substring(1) ) lineNum++ - } else if(i < oldLen) { + } else if (i < oldLen) { events += Event( ChangeType.Remove, lineNum, @@ -123,7 +124,7 @@ fun diffWalk(diff : Diff) : List { } } - fun stepChunk(chunk : List) { + fun stepChunk(chunk: List) { val header = chunk[0] // format of header is @@ -137,7 +138,7 @@ fun diffWalk(diff : Diff) : List { // of the file the new and old are the same, and since we add // and subtract lines as we go, we should stay in step with the // new offsets. - val newOffset = offsets[1].split(",").map{ + val newOffset = offsets[1].split(",").map { abs(it.toInt()) }.first() @@ -150,4 +151,4 @@ fun diffWalk(diff : Diff) : List { chunks.forEach(::stepChunk) return events -} \ No newline at end of file +} diff --git a/src/main/kotlin/me/msoucy/gbat/Repo.kt b/src/main/kotlin/me/msoucy/gbat/Repo.kt index 328781f..96b5f25 100644 --- a/src/main/kotlin/me/msoucy/gbat/Repo.kt +++ b/src/main/kotlin/me/msoucy/gbat/Repo.kt @@ -5,8 +5,8 @@ import java.io.IOException typealias Diff = String -class GitRepo(val projectRoot : File, val git_exe : String) { - fun ls() : String { +class GitRepo(val projectRoot: File, val git_exe: String) { + fun ls(): String { val cmd = listOf( git_exe, "ls-tree", @@ -20,7 +20,7 @@ class GitRepo(val projectRoot : File, val git_exe : String) { return out ?: "" } - fun root() : File { + fun root(): File { val cmd = listOf( git_exe, "rev-parse", @@ -30,7 +30,7 @@ class GitRepo(val projectRoot : File, val git_exe : String) { return File((out ?: "").trim()) } - fun log(fname : File) : List> { + fun log(fname: File): List> { val cmd = listOf( git_exe, "--no-pager", @@ -43,11 +43,11 @@ class GitRepo(val projectRoot : File, val git_exe : String) { fname.absolutePath ) val (out, err) = cmd.runCommand(projectRoot) - if(err != "") { + if (err != "") { System.err.println("Error from git log: " + err) throw IOException(err) } - val logEntries = (out?: "").split("\u0000").filter {it.trim().isNotEmpty()} + val logEntries = (out ?: "").split("\u0000").filter { it.trim().isNotEmpty() } return logEntries.map { val (header, diffLines) = splitEntryHeader(it) val diff = diffLines.joinToString("\n") @@ -58,25 +58,25 @@ class GitRepo(val projectRoot : File, val git_exe : String) { }.reversed() } - private fun parseAuthor(header : List) : String { - val segs = header.getOrNull(1)?.trim()?.split("\\s+".toRegex())?: listOf() + private fun parseAuthor(header: List): String { + val segs = header.getOrNull(1)?.trim()?.split("\\s+".toRegex()) ?: listOf() return segs.subList(1, segs.size - 1).joinToString(" ") } - private fun splitEntryHeader(entry : String) : Pair, List> { + private fun splitEntryHeader(entry: String): Pair, List> { val lines = entry.split("\n") - if(lines.size < 2) { + if (lines.size < 2) { return Pair(listOf(), listOf()) - } else if(!lines.get(0).startsWith("commit")) { + } else if (!lines.get(0).startsWith("commit")) { return Pair(listOf(), listOf()) - } else if(!lines.get(1).startsWith("Author")) { + } else if (!lines.get(1).startsWith("Author")) { return Pair(listOf(), listOf()) } var ind = 2 - while(ind < lines.size && !lines.get(ind).startsWith("diff")) { + while (ind < lines.size && !lines.get(ind).startsWith("diff")) { ind++ } return Pair(lines.subList(0, ind).copyOf(), lines.subList(ind, lines.size).copyOf()) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/me/msoucy/gbat/Util.kt b/src/main/kotlin/me/msoucy/gbat/Util.kt index 5127b28..47d2cf9 100644 --- a/src/main/kotlin/me/msoucy/gbat/Util.kt +++ b/src/main/kotlin/me/msoucy/gbat/Util.kt @@ -4,10 +4,10 @@ import java.io.File import java.io.IOException import java.util.concurrent.TimeUnit -fun Iterable.copyOf() : List = mutableListOf().also { it.addAll(this) } +fun Iterable.copyOf(): List = mutableListOf().also { it.addAll(this) } fun Iterable.mutableCopyOf() = mutableListOf().also { it.addAll(this) } -fun List.runCommand(workingDir: File): Pair { +fun List.runCommand(workingDir: File): Pair { try { val proc = ProcessBuilder(*this.toTypedArray()) .directory(workingDir) @@ -18,8 +18,8 @@ fun List.runCommand(workingDir: File): Pair { proc.waitFor(5, TimeUnit.SECONDS) return Pair(proc.inputStream.bufferedReader().readText(), proc.errorStream.bufferedReader().readText()) - } catch(e: IOException) { + } catch (e: IOException) { e.printStackTrace() return Pair(null, null) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/me/msoucy/gbat/models/KnowledgeModel.kt b/src/main/kotlin/me/msoucy/gbat/models/KnowledgeModel.kt index a24fbb0..00f329f 100644 --- a/src/main/kotlin/me/msoucy/gbat/models/KnowledgeModel.kt +++ b/src/main/kotlin/me/msoucy/gbat/models/KnowledgeModel.kt @@ -1,20 +1,18 @@ package me.msoucy.gbat.models -import java.io.File -import java.nio.file.Path -import java.nio.file.Paths +import me.msoucy.gbat.copyOf +import me.msoucy.gbat.mutableCopyOf import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.transaction -import me.msoucy.gbat.copyOf -import me.msoucy.gbat.mutableCopyOf +class KnowledgeModel(val db: Database, val constant: Double, val riskModel: RiskModel) { -class KnowledgeModel(val db : Database, val constant : Double, val riskModel : RiskModel) { - - class KnowledgeAcct(var knowledgeAcctId : Int, - var authors : List, - var authorsStr : String) + class KnowledgeAcct( + var knowledgeAcctId: Int, + var authors: List, + var authorsStr: String + ) object AuthorsTable : IntIdTable("authors", "authorid") { val author = text("author").uniqueIndex("authors_idx") @@ -41,13 +39,13 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R val SAFE_KNOWLEDGE_ACCT_ID = 1 val KNOWLEDGE_PER_LINE_ADDED = 1000.0 - fun apply(changeType : ChangeType, author : String, lineNum : Int) = when(changeType) { + fun apply(changeType: ChangeType, author: String, lineNum: Int) = when (changeType) { ChangeType.Add -> lineAdded(author, lineNum) ChangeType.Change -> lineChanged(author, lineNum) ChangeType.Remove -> lineRemoved(lineNum) } - fun lineChanged(author : String, lineNum : Int) { + fun lineChanged(author: String, lineNum: Int) { val kCreated = constant * KNOWLEDGE_PER_LINE_ADDED val kAcquired = (1 - constant) * KNOWLEDGE_PER_LINE_ADDED val totLineK = totalLineKnowledge(lineNum) @@ -59,20 +57,20 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R adjustKnowledge(knowledgeAcctId, lineNum, kCreated) } - fun lineRemoved(lineNum : Int) { + fun lineRemoved(lineNum: Int) { allAcctsWithKnowledgeOf(lineNum).forEach { destroyLineKnowledge(it, lineNum) } bumpAllLinesFrom(lineNum, -1) } - fun lineAdded(author : String, lineNum : Int) { + fun lineAdded(author: String, lineNum: Int) { val knowledgeAcctId = lookupOrCreateKnowledgeAcct(listOf(author)) - bumpAllLinesFrom(lineNum-1, 1) + bumpAllLinesFrom(lineNum - 1, 1) adjustKnowledge(knowledgeAcctId, lineNum, KNOWLEDGE_PER_LINE_ADDED) } - fun knowledgeSummary(lineNum : Int) = transaction(db) { + fun knowledgeSummary(lineNum: Int) = transaction(db) { LineKnowledge.select { LineKnowledge.linenum eq lineNum }.map { @@ -83,15 +81,15 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R }.copyOf() } - private fun bumpAllLinesFrom(lineNum : Int, adjustment : Int) = transaction(db) { - LineKnowledge.update({LineKnowledge.linenum greater lineNum}) { + private fun bumpAllLinesFrom(lineNum: Int, adjustment: Int) = transaction(db) { + LineKnowledge.update({ LineKnowledge.linenum greater lineNum }) { with(SqlExpressionBuilder) { it[LineKnowledge.linenum] = LineKnowledge.linenum + adjustment } } } - private fun getKnowledgeAcct(knowledgeAcctId : Int) : KnowledgeAcct = transaction(db) { + private fun getKnowledgeAcct(knowledgeAcctId: Int): KnowledgeAcct = transaction(db) { KnowledgeAcctsTable.select { KnowledgeAcctsTable.id eq knowledgeAcctId }.map { @@ -103,15 +101,15 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R }.firstOrNull() ?: KnowledgeAcct(-1, listOf(), "") } - private fun destroyLineKnowledge(knowledgeId : Int, lineNum : Int) = transaction(db) { + private fun destroyLineKnowledge(knowledgeId: Int, lineNum: Int) = transaction(db) { LineKnowledge.deleteWhere { (LineKnowledge.knowledgeacctid eq knowledgeId) and (LineKnowledge.linenum eq lineNum) } } - private fun redistributeKnowledge(author : String, lineNum : Int, redistPct : Double) { - if(riskModel.isDeparted(author)) { + private fun redistributeKnowledge(author: String, lineNum: Int, redistPct: Double) { + if (riskModel.isDeparted(author)) { return } val knowledgeIds = nonSafeAcctsWithKnowledgeOf(lineNum) @@ -120,13 +118,13 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R if (author !in knowledgeAcct.authors) { val oldKnowledge = knowledgeInAcct(knowledgeAcct.knowledgeAcctId, lineNum) var newAuthors = knowledgeAcct.authors.mutableCopyOf() - if(newAuthors.all(riskModel::isDeparted)) { + if (newAuthors.all(riskModel::isDeparted)) { newAuthors = mutableListOf(author) } else { newAuthors.add(author) } newAuthors = newAuthors.sorted().mutableCopyOf() - val newKnowledgeId = if(riskModel.jointBusProbBelowThreshold(newAuthors)) { + val newKnowledgeId = if (riskModel.jointBusProbBelowThreshold(newAuthors)) { SAFE_KNOWLEDGE_ACCT_ID } else { lookupOrCreateKnowledgeAcct(newAuthors) @@ -138,7 +136,7 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R } } - private fun knowledgeInAcct(knowledgeAcctId : Int, lineNum : Int) = transaction(db) { + private fun knowledgeInAcct(knowledgeAcctId: Int, lineNum: Int) = transaction(db) { LineKnowledge.select { (LineKnowledge.knowledgeacctid eq knowledgeAcctId) and (LineKnowledge.linenum eq lineNum) @@ -147,7 +145,7 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R }.first() } - private fun nonSafeAcctsWithKnowledgeOf(lineNum : Int) = transaction(db) { + private fun nonSafeAcctsWithKnowledgeOf(lineNum: Int) = transaction(db) { LineKnowledge.select { (LineKnowledge.linenum eq lineNum) and (LineKnowledge.knowledgeacctid neq SAFE_KNOWLEDGE_ACCT_ID) @@ -156,7 +154,7 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R } } - private fun allAcctsWithKnowledgeOf(lineNum : Int) = transaction(db) { + private fun allAcctsWithKnowledgeOf(lineNum: Int) = transaction(db) { LineKnowledge.select { LineKnowledge.linenum eq lineNum }.map { @@ -164,12 +162,12 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R } } - private fun adjustKnowledge(knowledgeAcctId : Int, lineNum : Int, adjustment : Double) = transaction(db) { + private fun adjustKnowledge(knowledgeAcctId: Int, lineNum: Int, adjustment: Double) = transaction(db) { val lineExists = LineKnowledge.select { (LineKnowledge.knowledgeacctid eq knowledgeAcctId) and (LineKnowledge.linenum eq lineNum) }.count() > 0 - if(!lineExists) { + if (!lineExists) { LineKnowledge.insert { it[LineKnowledge.knowledgeacctid] = knowledgeAcctId it[LineKnowledge.linenum] = lineNum @@ -179,21 +177,21 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R LineKnowledge.update({ (LineKnowledge.knowledgeacctid eq knowledgeAcctId) and (LineKnowledge.linenum eq lineNum) - }) { + }) { with(SqlExpressionBuilder) { it[LineKnowledge.knowledge] = LineKnowledge.knowledge + adjustment } } } - private fun lookupOrCreateKnowledgeAcct(authors : List) = transaction(db) { + private fun lookupOrCreateKnowledgeAcct(authors: List) = transaction(db) { val authorStr = authors.sorted().joinToString("\n") KnowledgeAcctsTable.select { KnowledgeAcctsTable.authors eq authorStr }.map { it[KnowledgeAcctsTable.id].value }.firstOrNull() ?: run { - KnowledgeAcctsTable.insert { + KnowledgeAcctsTable.insert { it[KnowledgeAcctsTable.authors] = authorStr } val theNewId = KnowledgeAcctsTable.select { @@ -212,8 +210,8 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R } } - private fun lookupOrCreateAuthor(authorName : String) = transaction(db) { - AuthorsTable.insertIgnore { + private fun lookupOrCreateAuthor(authorName: String) = transaction(db) { + AuthorsTable.insertIgnore { it[author] = authorName } AuthorsTable.select { @@ -222,8 +220,8 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R it[AuthorsTable.id] } } - - private fun totalLineKnowledge(linenum : Int) = transaction(db) { + + private fun totalLineKnowledge(linenum: Int) = transaction(db) { LineKnowledge.select { LineKnowledge.linenum eq linenum }.map { diff --git a/src/main/kotlin/me/msoucy/gbat/models/LineModel.kt b/src/main/kotlin/me/msoucy/gbat/models/LineModel.kt index fcfeeb7..1b3a69d 100644 --- a/src/main/kotlin/me/msoucy/gbat/models/LineModel.kt +++ b/src/main/kotlin/me/msoucy/gbat/models/LineModel.kt @@ -1,42 +1,34 @@ package me.msoucy.gbat.models -import java.io.File -import java.nio.file.Path -import java.nio.file.Paths -import kotlin.io.forEachLine -import org.jetbrains.exposed.dao.id.IntIdTable -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.transactions.transaction - class LineModel() { - inner class Line(var num : Int, var text : String) + inner class Line(var num: Int, var text: String) val model = mutableSetOf() - fun apply(changeType : ChangeType, lineNum : Int, lineText : String) = when(changeType) { + fun apply(changeType: ChangeType, lineNum: Int, lineText: String) = when (changeType) { ChangeType.Add -> add(Line(lineNum, lineText)) ChangeType.Change -> change(Line(lineNum, lineText)) ChangeType.Remove -> del(Line(lineNum, lineText)) } - fun add(line : Line) { + fun add(line: Line) { model.onEach { entry -> - if(entry.num >= line.num) { + if (entry.num >= line.num) { entry.num++ } } model.add(line) } - fun del(line : Line) { + fun del(line: Line) { model.removeIf { it.num == line.num } model.onEach { entry -> - if(entry.num > line.num) { + if (entry.num > line.num) { entry.num-- } } } - fun change(line : Line) { + fun change(line: Line) { model.removeIf { it.num == line.num } model.add(line) } diff --git a/src/main/kotlin/me/msoucy/gbat/models/Models.kt b/src/main/kotlin/me/msoucy/gbat/models/Models.kt index 5f42f82..ded5594 100644 --- a/src/main/kotlin/me/msoucy/gbat/models/Models.kt +++ b/src/main/kotlin/me/msoucy/gbat/models/Models.kt @@ -7,37 +7,37 @@ enum class ChangeType { } data class Event( - val eventType : ChangeType, - val lineNum : Int, - val lineVal : String? + val eventType: ChangeType, + val lineNum: Int, + val lineVal: String? ) data class Condensation( - val authors : List, - val knowledge : Double, - val orphaned : Double, - val risk : Double = 0.0 + val authors: List, + val knowledge: Double, + val orphaned: Double, + val risk: Double = 0.0 ) : Comparable { - override operator fun compareTo(other : Condensation) : Int { + override operator fun compareTo(other: Condensation): Int { var result = authors.size.compareTo(other.authors.size) - if(result == 0) { + if (result == 0) { authors.zip(other.authors).forEach { (a, b) -> - if(result == 0) result = a.compareTo(b) + if (result == 0) result = a.compareTo(b) } } - if(result == 0) + if (result == 0) result = knowledge.compareTo(other.knowledge) - if(result == 0) + if (result == 0) result = orphaned.compareTo(other.orphaned) - if(result == 0) + if (result == 0) result = risk.compareTo(other.risk) return result } } class CondensedAnalysis( - var repoRoot : File, - var projectRoot : File, - var fileName : File, - var lineSummaries : MutableList>> = mutableListOf() -) \ No newline at end of file + var repoRoot: File, + var projectRoot: File, + var fileName: File, + var lineSummaries: MutableList>> = mutableListOf() +) diff --git a/src/main/kotlin/me/msoucy/gbat/models/RiskModel.kt b/src/main/kotlin/me/msoucy/gbat/models/RiskModel.kt index 42a1233..13998a2 100644 --- a/src/main/kotlin/me/msoucy/gbat/models/RiskModel.kt +++ b/src/main/kotlin/me/msoucy/gbat/models/RiskModel.kt @@ -3,50 +3,52 @@ package me.msoucy.gbat.models import java.io.File import kotlin.io.forEachLine -class RiskModel(val threshold : Double, - val default : Double, - val busRiskFile : File?, - val departedFile : File?) { +class RiskModel( + val threshold: Double, + val default: Double, + val busRiskFile: File?, + val departedFile: File? +) { val departed = mutableSetOf() - val risks = mutableMapOf().withDefault {default} + val risks = mutableMapOf().withDefault { default } init { parseBusRisks() parseDeparted() } - operator fun get(author : String) : Double { + operator fun get(author: String): Double { val name = author.trim() - if(name.isEmpty()) { + if (name.isEmpty()) { return threshold } return risks.getOrPut(name) { default } } - fun isDeparted(author : String) = author.trim() in departed + fun isDeparted(author: String) = author.trim() in departed - fun jointBusProb(authors : List) = + fun jointBusProb(authors: List) = (authors.map { this[it] } + 1.0).reduce { a, b -> a * b } - fun jointBusProbBelowThreshold(authors : List) = + fun jointBusProbBelowThreshold(authors: List) = jointBusProb(authors) <= threshold - + private fun parseBusRisks() { busRiskFile?.forEachLine { line -> val sline = line.trim() - if(sline.isNotEmpty()) { + if (sline.isNotEmpty()) { val segments = sline.split("=") val risk = segments.last() - val author = segments.dropLast(1).joinToString(separator="=") + val author = segments.dropLast(1).joinToString(separator = "=") risks[author] = risk.toDouble() } } } - + private fun parseDeparted() { departedFile?.forEachLine { line -> val author = line.trim() - if(author.isNotEmpty()) { + if (author.isNotEmpty()) { risks[author] = 1.0 departed.add(author) } diff --git a/src/main/kotlin/me/msoucy/gbat/models/SummaryModel.kt b/src/main/kotlin/me/msoucy/gbat/models/SummaryModel.kt index 69f6d5d..0e0b738 100644 --- a/src/main/kotlin/me/msoucy/gbat/models/SummaryModel.kt +++ b/src/main/kotlin/me/msoucy/gbat/models/SummaryModel.kt @@ -55,7 +55,7 @@ class FileTree { var authorRisks = mutableMapOf() var lines = mutableListOf() } -class FileEntry(var name : String = "") { +class FileEntry(var name: String = "") { var stats = Statistics() var authorRisks = mutableMapOf() } @@ -64,14 +64,14 @@ class ProjectTree { var files = mutableMapOf() var dirs = mutableListOf() } -class ProjectFilesResult(var fileId : Int, var fname : Path) +class ProjectFilesResult(var fileId: Int, var fname: Path) data class Statistics( - var totKnowledge : Double = 0.0, - var totRisk : Double = 0.0, - var totOrphaned : Double = 0.0 + var totKnowledge: Double = 0.0, + var totRisk: Double = 0.0, + var totOrphaned: Double = 0.0 ) { - constructor(row : ResultRow) : + constructor(row: ResultRow) : this(row[AllocationsTable.knowledge.sum()] ?: 0.0, row[AllocationsTable.risk.sum()] ?: 0.0, row[AllocationsTable.orphaned.sum()] ?: 0.0) {} @@ -81,25 +81,25 @@ class ProjectTreeNode { var files = mutableListOf() var dirs = mutableListOf() } -class ProjectTreeResult(var name : String, var root : ProjectTreeNode) { +class ProjectTreeResult(var name: String, var root: ProjectTreeNode) { var stats = Statistics() var authorRisks = mutableMapOf() } -class SummaryModel(val db : Database) { +class SummaryModel(val db: Database) { val GIT_BY_A_BUS_BELOW_THRESHOLD = "Git by a Bus Safe Author" init { createTables() } - + private val lineAllocations = (LinesTable leftJoin AllocationsTable) private val lineAllocationGroups = (lineAllocations leftJoin AuthorsGroupsTable) private val manyJoined = (lineAllocations leftJoin FilesTable leftJoin DirsTable) private val allJoined = (manyJoined leftJoin AuthorsGroupsTable) - fun summarize(ca : CondensedAnalysis) { + fun summarize(ca: CondensedAnalysis) { val fname = adjustFname(ca.repoRoot.absoluteFile, ca.projectRoot.absoluteFile, ca.fileName.absoluteFile) @@ -122,7 +122,7 @@ class SummaryModel(val db : Database) { } } } - + fun totalKnowledge() = transaction(db) { AllocationsTable.selectAll().map { it[AllocationsTable.knowledge] }.sum() } @@ -139,12 +139,12 @@ class SummaryModel(val db : Database) { FilesTable.selectAll().count() } - fun authorgroupsWithRisk(top : Int? = null) : List> = transaction(db) { + fun authorgroupsWithRisk(top: Int? = null): List> = transaction(db) { var query = (AllocationsTable innerJoin AuthorsGroupsTable) .selectAll() .groupBy(AuthorsGroupsTable.authors) .orderBy(AllocationsTable.risk.sum() to SortOrder.DESC) - if(top != null) { + if (top != null) { query = query.limit(top) } query.map { @@ -152,12 +152,12 @@ class SummaryModel(val db : Database) { } } - fun fileidsWithRisk(top : Int? = null) : List> = transaction(db) { + fun fileidsWithRisk(top: Int? = null): List> = transaction(db) { var query = (FilesTable leftJoin LinesTable leftJoin AllocationsTable) .selectAll() .groupBy(FilesTable.id) .orderBy(AllocationsTable.risk.sum() to SortOrder.DESC) - if(top != null) { + if (top != null) { query = query.limit(top) } query.map { @@ -165,7 +165,7 @@ class SummaryModel(val db : Database) { } } - fun fpath(fileId : Int) : Path = transaction(db) { + fun fpath(fileId: Int): Path = transaction(db) { FilesTable.select { FilesTable.id eq fileId }.first().let { row -> @@ -174,7 +174,7 @@ class SummaryModel(val db : Database) { } } - fun projectFiles(project : String) : List = transaction(db) { + fun projectFiles(project: String): List = transaction(db) { val projectId = findOrCreateProject(project) (FilesTable innerJoin DirsTable).select { (FilesTable.dirid eq DirsTable.id) and @@ -184,13 +184,13 @@ class SummaryModel(val db : Database) { } } - fun projectSummary(project : String) = transaction(db) { + fun projectSummary(project: String) = transaction(db) { val projectId = findOrCreateProject(project) - val theTree = mutableMapOf().withDefault {ProjectTree()} + val theTree = mutableMapOf().withDefault { ProjectTree() } // First fill in the directory structure, ignoring the files val parentDirIds = mutableListOf(0) - while(parentDirIds.isNotEmpty()) { + while (parentDirIds.isNotEmpty()) { val parentId = parentDirIds.removeAt(0) DirsTable.select { DirsTable.parentdirid eq parentId } .forEach { row -> @@ -265,7 +265,7 @@ class SummaryModel(val db : Database) { projectTree } - fun fileSummary(fileId : Int) = transaction(db) { + fun fileSummary(fileId: Int) = transaction(db) { var fileTree = FileTree() lineAllocationGroups .slice(AllocationsTable.knowledge.sum(), AllocationsTable.risk.sum(), AllocationsTable.orphaned.sum(), AuthorsGroupsTable.authors) @@ -311,7 +311,7 @@ class SummaryModel(val db : Database) { fileTree } - fun fileLines(fileId : Int) : List = transaction(db) { + fun fileLines(fileId: Int): List = transaction(db) { LinesTable.select { LinesTable.fileid eq fileId }.orderBy(LinesTable.linenum).map { @@ -319,7 +319,7 @@ class SummaryModel(val db : Database) { } } - private fun transformNode(tree : MutableMap, dirId : Int) : ProjectTreeNode { + private fun transformNode(tree: MutableMap, dirId: Int): ProjectTreeNode { val result = ProjectTreeNode() tree[dirId]?.let { dirdict -> result.name = dirdict.name @@ -334,10 +334,10 @@ class SummaryModel(val db : Database) { return result } - private fun reconsDir(dirId : Int) = transaction(db) { + private fun reconsDir(dirId: Int) = transaction(db) { val segs = mutableListOf() var newDirId = dirId - while(newDirId != 0) { + while (newDirId != 0) { DirsTable.select { DirsTable.id eq newDirId }.forEach { @@ -348,9 +348,9 @@ class SummaryModel(val db : Database) { Paths.get(segs.reversed().joinToString("/")).normalize() } - private fun safeAuthorName(author : String?) = author ?: GIT_BY_A_BUS_BELOW_THRESHOLD + private fun safeAuthorName(author: String?) = author ?: GIT_BY_A_BUS_BELOW_THRESHOLD - private fun createAllocation(knowledge : Double, risk : Double, orphaned : Double, authorGroupId : Int, lineId : Int) = transaction(db) { + private fun createAllocation(knowledge: Double, risk: Double, orphaned: Double, authorGroupId: Int, lineId: Int) = transaction(db) { AllocationsTable.insert { it[AllocationsTable.knowledge] = knowledge it[AllocationsTable.risk] = risk @@ -360,7 +360,7 @@ class SummaryModel(val db : Database) { } } - private fun findOrCreateAuthorGroup(authors : List) : Int = transaction(db) { + private fun findOrCreateAuthorGroup(authors: List): Int = transaction(db) { val authorsstr = authors.joinToString("\n") var authorGroupId = AuthorsGroupsTable.select { AuthorsGroupsTable.authors eq authorsstr @@ -383,7 +383,7 @@ class SummaryModel(val db : Database) { authorGroupId } - private fun findOrCreateAuthor(author : String) : Int = transaction(db) { + private fun findOrCreateAuthor(author: String): Int = transaction(db) { AuthorsTable.insertIgnore { it[AuthorsTable.author] = author } @@ -394,7 +394,7 @@ class SummaryModel(val db : Database) { }.first() } - private fun createLine(line : String, lineNum : Int, fileId : Int) = transaction(db) { + private fun createLine(line: String, lineNum: Int, fileId: Int) = transaction(db) { LinesTable.insertAndGetId { it[LinesTable.line] = line it[LinesTable.linenum] = lineNum @@ -402,14 +402,14 @@ class SummaryModel(val db : Database) { }.value } - private fun createFile(fname : String, parentDirId : Int) = transaction(db) { + private fun createFile(fname: String, parentDirId: Int) = transaction(db) { FilesTable.insertAndGetId { it[FilesTable.fname] = fname it[FilesTable.dirid] = parentDirId }.value } - private fun findOrCreateProject(project : String) : Int = transaction(db) { + private fun findOrCreateProject(project: String): Int = transaction(db) { ProjectTable.insertIgnore { it[ProjectTable.project] = project } @@ -420,7 +420,7 @@ class SummaryModel(val db : Database) { }.first() } - private fun findOrCreateDir(dirname : String, projectId : Int, parentDirId : Int) : Int = transaction(db) { + private fun findOrCreateDir(dirname: String, projectId: Int, parentDirId: Int): Int = transaction(db) { DirsTable.insertIgnore { it[dir] = dirname it[parentdirid] = parentDirId @@ -435,25 +435,25 @@ class SummaryModel(val db : Database) { }.first() } - private fun splitAllDirs(dirname : File) = dirname.toPath().iterator().asSequence().toList() + private fun splitAllDirs(dirname: File) = dirname.toPath().iterator().asSequence().toList() - private fun adjustFname(repoRoot : File, projectRoot : File, fname : File) : File { - val rootDiff = if(projectRoot.canonicalPath != repoRoot.canonicalPath) { + private fun adjustFname(repoRoot: File, projectRoot: File, fname: File): File { + val rootDiff = if (projectRoot.canonicalPath != repoRoot.canonicalPath) { projectRoot.relativeTo(repoRoot) } else { repoRoot } - return if(rootDiff.toString().length != 0) { + return if (rootDiff.toString().length != 0) { fname.relativeTo(rootDiff) } else { fname } } - private fun reconsDirs(dirId : Int) = transaction(db) { + private fun reconsDirs(dirId: Int) = transaction(db) { val dirs = mutableListOf() var parentDirId = dirId - while(parentDirId != 0) { + while (parentDirId != 0) { DirsTable.select { DirsTable.id eq parentDirId } @@ -477,4 +477,4 @@ class SummaryModel(val db : Database) { AllocationsTable ) } -} \ No newline at end of file +}