Compare commits

...

10 Commits

Author SHA1 Message Date
d049197d7c Update for modern Exposed 2024-02-24 10:27:55 -05:00
da9a4e9602 Add dependency 2024-02-21 21:00:28 -05:00
36a3183fd3 Modernize bump 2024-02-21 20:57:40 -05:00
e897278323 Ignore output directory 2020-08-27 20:50:15 -04:00
a6f0c57c16 Make parallel actually parallel 2020-08-27 20:37:58 -04:00
ce969aa25c Fix root 2020-08-18 21:29:05 -04:00
8dd083aae5 Rendering 2020-08-18 21:24:24 -04:00
1159b87a53 Formatting 2020-08-18 21:01:00 -04:00
b85099fbbd Fix authors 2020-07-03 22:11:59 -04:00
6569ec619a Fix name 2020-07-03 21:26:41 -04:00
18 changed files with 578 additions and 515 deletions

View File

@ -1,30 +0,0 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version "$kotlinVersion"
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlinVersion"
id 'application'
}
version = '0.1.1'
mainClassName = 'me.msoucy.gbat.MainKt'
description = 'Git By A Truck'
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
jcenter()
}
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
implementation "com.xenomachina:kotlin-argparser:$kotlin_argparser_version"
implementation "org.jetbrains.exposed:exposed-core:$kotlin_exposed_version"
implementation "org.jetbrains.exposed:exposed-dao:$kotlin_exposed_version"
implementation "org.jetbrains.exposed:exposed-jdbc:$kotlin_exposed_version"
implementation "org.xerial:sqlite-jdbc:3.30.1"
implementation 'com.google.code.gson:gson:2.8.6'
testImplementation 'junit:junit:4.12'
}

39
build.gradle.kts Normal file
View File

@ -0,0 +1,39 @@
plugins {
kotlin("jvm") version "1.9.22"
kotlin("plugin.serialization") version "1.9.22"
application
}
version = "0.1.1"
group = "me.msoucy.gbat"
description = "Git By A Truck"
kotlin {
jvmToolchain(21)
}
repositories {
mavenCentral()
}
val argparserVersion: String by project
val exposedVersion: String by project
dependencies {
implementation(kotlin("reflect"))
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
implementation("com.xenomachina:kotlin-argparser:$argparserVersion")
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
implementation("org.xerial:sqlite-jdbc:3.45.1.0")
implementation("com.google.code.gson:gson:2.10.1")
implementation("org.python:jython-standalone:2.7.2")
implementation("org.pygments:pygments:2.5.2")
testImplementation("junit:junit:4.12")
}
application {
mainClass.set("MainKt") // The main class of the application
}

View File

@ -1,3 +1,3 @@
kotlinVersion=1.3.71 kotlinVersion=1.4.0
kotlin_argparser_version=2.0.7 argparserVersion=2.0.7
kotlin_exposed_version=0.25.1 exposedVersion=0.47.0

Binary file not shown.

View File

@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

288
gradlew vendored
View File

@ -1,7 +1,7 @@
#!/usr/bin/env sh #!/bin/sh
# #
# Copyright 2015 the original author or authors. # Copyright © 2015-2021 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -17,67 +17,98 @@
# #
############################################################################## ##############################################################################
## #
## Gradle start up script for UN*X # Gradle start up script for POSIX generated by Gradle.
## #
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
############################################################################## ##############################################################################
# Attempt to set APP_HOME # Attempt to set APP_HOME
# Resolve links: $0 may be a link # Resolve links: $0 may be a link
PRG="$0" app_path=$0
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do # Need this for daisy-chained symlinks.
ls=`ls -ld "$PRG"` while
link=`expr "$ls" : '.*-> \(.*\)$'` APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
if expr "$link" : '/.*' > /dev/null; then [ -h "$app_path" ]
PRG="$link" do
else ls=$( ls -ld "$app_path" )
PRG=`dirname "$PRG"`"/$link" link=${ls#*' -> '}
fi case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle" # This is normally unused
APP_BASE_NAME=`basename "$0"` # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD=maximum
warn () { warn () {
echo "$*" echo "$*"
} } >&2
die () { die () {
echo echo
echo "$*" echo "$*"
echo echo
exit 1 exit 1
} } >&2
# OS specific support (must be 'true' or 'false'). # OS specific support (must be 'true' or 'false').
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "`uname`" in case "$( uname )" in #(
CYGWIN* ) CYGWIN* ) cygwin=true ;; #(
cygwin=true Darwin* ) darwin=true ;; #(
;; MSYS* | MINGW* ) msys=true ;; #(
Darwin* ) NONSTOP* ) nonstop=true ;;
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@ -87,9 +118,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables # IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java" JAVACMD=$JAVA_HOME/jre/sh/java
else else
JAVACMD="$JAVA_HOME/bin/java" JAVACMD=$JAVA_HOME/bin/java
fi fi
if [ ! -x "$JAVACMD" ] ; then if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -98,7 +129,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
else else
JAVACMD="java" JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
@ -106,80 +137,109 @@ location of your Java installation."
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
MAX_FD_LIMIT=`ulimit -H -n` case $MAX_FD in #(
if [ $? -eq 0 ] ; then max*)
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
MAX_FD="$MAX_FD_LIMIT" # shellcheck disable=SC3045
fi MAX_FD=$( ulimit -H -n ) ||
ulimit -n $MAX_FD warn "Could not query maximum file descriptor limit"
if [ $? -ne 0 ] ; then esac
warn "Could not set maximum file descriptor limit: $MAX_FD" case $MAX_FD in #(
fi '' | soft) :;; #(
else *)
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
fi # shellcheck disable=SC3045
fi ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac esac
fi fi
# Escape application args # Collect all arguments for the java command, stacking in reverse order:
save () { # * args from the command line
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done # * the main class name
echo " " # * -classpath
} # * -D...appname settings
APP_ARGS=`save "$@"` # * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules # For Cygwin or MSYS, switch paths to Windows format before running java
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"

34
gradlew.bat vendored
View File

@ -14,7 +14,7 @@
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@if "%DEBUG%" == "" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init if %ERRORLEVEL% equ 0 goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -54,7 +55,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init if exist "%JAVA_EXE%" goto execute
echo. echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@ -64,21 +65,6 @@ echo location of your Java installation.
goto fail goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute :execute
@rem Setup the command line @rem Setup the command line
@ -86,17 +72,19 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd if %ERRORLEVEL% equ 0 goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 set EXIT_CODE=%ERRORLEVEL%
exit /b 1 if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal

View File

@ -1,21 +1,20 @@
package me.msoucy.gbat package me.msoucy.gbat
import java.io.File 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.Condensation
import me.msoucy.gbat.models.CondensedAnalysis
import me.msoucy.gbat.models.KnowledgeModel import me.msoucy.gbat.models.KnowledgeModel
import me.msoucy.gbat.models.LineModel import me.msoucy.gbat.models.LineModel
import me.msoucy.gbat.models.RiskModel import me.msoucy.gbat.models.RiskModel
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
fun analyze( fun analyze(
riskModel : RiskModel, riskModel: RiskModel,
createdConstant : Double, createdConstant: Double,
historyItem : HistoryItem, historyItem: HistoryItem,
verbose : Boolean = false verbose: Boolean = false
) : CondensedAnalysis { ): CondensedAnalysis {
val lineModel = LineModel() val lineModel = LineModel()
val db = Database.connect("jdbc:sqlite::memory:", "org.sqlite.JDBC") val db = Database.connect("jdbc:sqlite::memory:", "org.sqlite.JDBC")
return transaction(db) { return transaction(db) {
@ -25,8 +24,8 @@ fun analyze(
historyItem.authorDiffs.forEach { (author, changes) -> historyItem.authorDiffs.forEach { (author, changes) ->
changes.forEach { change -> changes.forEach { change ->
changesProcessed++ changesProcessed++
if(changesProcessed % 1000 == 0 && verbose) { if (changesProcessed % 1000 == 0 && verbose) {
System.err.println("Analyzer applied change #${changesProcessed}") System.err.println("Analyzer applied change #$changesProcessed")
} }
lineModel.apply(change.eventType, change.lineNum, change.lineVal ?: "") lineModel.apply(change.eventType, change.lineNum, change.lineVal ?: "")
knowledgeModel.apply(change.eventType, author, change.lineNum) knowledgeModel.apply(change.eventType, author, change.lineNum)
@ -44,21 +43,21 @@ fun analyze(
} }
private fun condenseAnalysis( private fun condenseAnalysis(
repoRoot : File, repoRoot: File,
projectRoot : File, projectRoot: File,
fname : File, fname: File,
lineModel : LineModel, lineModel: LineModel,
knowledgeModel : KnowledgeModel, knowledgeModel: KnowledgeModel,
riskModel : RiskModel riskModel: RiskModel
) : CondensedAnalysis { ): CondensedAnalysis {
val condensations = lineModel.get().mapIndexed { idx, line -> val condensations = lineModel.get().mapIndexed { idx, line ->
val knowledges = knowledgeModel.knowledgeSummary(idx + 1).map { (authors, knowledge) -> val knowledges = knowledgeModel.knowledgeSummary(idx + 1).map { (authors, knowledge) ->
Condensation(authors, Condensation(authors,
knowledge, knowledge,
if(authors.all(riskModel::isDeparted)) knowledge else 0.0, if (authors.all(riskModel::isDeparted)) knowledge else 0.0,
riskModel.jointBusProb(authors) * knowledge) riskModel.jointBusProb(authors) * knowledge)
}.sorted() }.sorted()
Pair(line, knowledges) Pair(line, knowledges)
} }
return CondensedAnalysis(repoRoot, projectRoot, fname, condensations.mutableCopyOf()) return CondensedAnalysis(repoRoot, projectRoot, fname, condensations.mutableCopyOf())
} }

View File

@ -1,8 +1,9 @@
package me.msoucy.gbat package me.msoucy.gbat
import me.msoucy.gbat.models.RiskModel import com.xenomachina.argparser.ArgParser
import me.msoucy.gbat.models.SummaryModel import com.xenomachina.argparser.InvalidArgumentException
import com.xenomachina.argparser.default
import com.xenomachina.argparser.mainBody
import java.io.File import java.io.File
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.math.pow import kotlin.math.pow
@ -12,90 +13,81 @@ import kotlin.text.RegexOption
import kotlin.text.startsWith import kotlin.text.startsWith
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import me.msoucy.gbat.models.RiskModel
import com.xenomachina.argparser.ArgParser import me.msoucy.gbat.models.SummaryModel
import com.xenomachina.argparser.InvalidArgumentException
import com.xenomachina.argparser.default
import com.xenomachina.argparser.mainBody
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
val REALLY_LONG_TIME = 864000 val REALLY_LONG_TIME = 864000
val DEFAULT_INTERESTING_RES = mutableListOf( val DEFAULT_INTERESTING_RES = mutableListOf(
"\\.java$", "\\.java$",
"\\.cs$", "\\.cs$",
"\\.py$", "\\.py$",
"\\.c$", "\\.c$",
"\\.cpp$", "\\.cpp$",
"\\.h$", "\\.h$",
"\\.hpp$", "\\.hpp$",
"\\.pl$", "\\.pl$",
"\\.perl$", "\\.perl$",
"\\.rb$", "\\.rb$",
"\\.sh$", "\\.sh$",
"\\.js$", "\\.js$",
"\\.kt$" "\\.kt$"
) )
fun validateGit(exe : String) : String { fun validateGit(exe: String): String {
val os = System.getProperty("os.name") 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" exe + ".exe"
} else exe } else exe
var file = File(fullexe) var file = File(fullexe)
if(file.canRead()) { if (file.canRead()) {
return file.absolutePath return file.absolutePath
} }
for(path in System.getenv("PATH").split(";")) { for (path in System.getenv("PATH").split(";")) {
file = File(path, fullexe) file = File(path, fullexe)
if(file.canRead()) { if (file.canRead()) {
return file.absolutePath return file.absolutePath
} }
} }
throw InvalidArgumentException("Provided git executable does not exist") throw InvalidArgumentException("Provided git executable does not exist")
} }
class GbatArgs(parser: ArgParser) { class GbatArgs(parser: ArgParser) {
// Input options // Input options
val interesting by parser.adding("--interesting", "-I", val interesting by parser.adding("--interesting", "-I", help = "Regular expression to determine which files should be included in calculations.")
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 not_interesting by parser.adding("--not-interesting", "-N", val case_sensitive by parser.flagging("Use case sensitive regexps when determining interesting files (default is case-insensitive)")
help="Regular expression to determine which files should not be included in calculations.") val departed by parser.storing("--departed-file", "-D", help = "File listing departed devs, one per line", transform = ::File).default<File?>(null)
val case_sensitive by parser.flagging("Use case sensitive regexps when determining interesting files (default is case-insensitive)") 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<File?>(null)
val departed by parser.storing("--departed-file", "-D", help="File listing departed devs, one per line", transform=::File).default<File?>(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)
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<File?>(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 // 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)
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 // 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<Double?>(null) val risk_threshold by parser.storing("--risk-threshold", help = "Threshold past which to summarize risk (defaults to default bus risk cubed)") { toDouble() }.default<Double?>(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) 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 // Misc options
val git_exe by parser.storing("--git-exe", help="Path to the git executable", transform=::validateGit).default("git").addValidator { validateGit(value) } 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 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") 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") val project_root by parser.positional("The root directory to inspect")
} }
fun main(args: Array<String>) = mainBody { fun main(args: Array<String>) = mainBody {
ArgParser(args).parseInto(::GbatArgs).run { ArgParser(args).parseInto(::GbatArgs).run {
val outDir = File(output) val outDir = File(output)
if(outDir.isDirectory) {
//throw InvalidArgumentException("Output directory already exists")
}
outDir.delete()
outDir.mkdirs() outDir.mkdirs()
File(outDir, ".gitignore").writeText("*")
fun parse_interesting(theList : List<String>) = fun parse_interesting(theList: List<String>) =
theList.map { theList.map {
if(case_sensitive) { if (case_sensitive) {
Regex(it) Regex(it)
} else { } else {
Regex(it, RegexOption.IGNORE_CASE) Regex(it, RegexOption.IGNORE_CASE)
@ -107,22 +99,20 @@ fun main(args: Array<String>) = mainBody {
val not_interesting_res = if (not_interesting.isEmpty()) listOf() else parse_interesting(not_interesting) val not_interesting_res = if (not_interesting.isEmpty()) listOf() else parse_interesting(not_interesting)
val projectRootFile = File(project_root).also { val projectRootFile = File(project_root).also {
if(!it.isDirectory) if (!it.isDirectory)
throw InvalidArgumentException("Provided project root does not exist") throw InvalidArgumentException("Provided project root does not exist")
} }
val repo = GitRepo(projectRootFile, validateGit(git_exe)) val repo = GitRepo(projectRootFile, validateGit(git_exe))
fun String.isInteresting() : Boolean { fun String.isInteresting(): Boolean {
var hasInterest = interesting_res.any { it.containsMatchIn(this) } var hasInterest = interesting_res.any { it.containsMatchIn(this) }
if(hasInterest) { if (hasInterest) {
hasInterest = !not_interesting_res.any { it.containsMatchIn(this) } hasInterest = !not_interesting_res.any { it.containsMatchIn(this) }
} }
return hasInterest return hasInterest
} }
val fnames = repo.ls().split("\n").filter { it.isInteresting() }
fun GitRepo.interestingNames() = ls().split("\n").filter{ it.isInteresting() }
val fnames = repo.interestingNames()
if (fnames.isEmpty()) { if (fnames.isEmpty()) {
System.err.println("No interesting files found, exiting.") System.err.println("No interesting files found, exiting.")
@ -136,30 +126,34 @@ fun main(args: Array<String>) = mainBody {
val riskModel = RiskModel(riskThresh, default_bus_risk, risk_file, departed) val riskModel = RiskModel(riskThresh, default_bus_risk, risk_file, departed)
val dbFname = File(outDir, "summary.db") val dbFname = File(outDir, "summary.db")
dbFname.delete(); dbFname.delete()
val summaryDb = Database.connect("jdbc:sqlite:${dbFname.absolutePath}", driver="org.sqlite.JDBC") 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")
}
val summaryModel = SummaryModel(summaryDb) val summaryModel = SummaryModel(summaryDb)
val analysisExecutor = Executors.newFixedThreadPool(num_analyzer_procs).asCoroutineDispatcher()
runBlocking { runBlocking {
flow { fnames.map { fname ->
fnames.forEach { fname -> async(analysisExecutor) {
emit(parseHistory(repo, projectRootFile, File(fname))) if (verbose) {
println("Analyzing $fname")
}
val history = parseHistory(repo, projectRootFile, File(projectRootFile, fname))
analyze(riskModel, creation_constant, history, verbose)
}
}.awaitAll().forEach { analysis ->
if (verbose) {
println("Summarizing ${analysis.fileName}")
} }
}.map { history ->
analyze(riskModel, creation_constant, history, verbose)
}.collect { analysis ->
summaryModel.summarize(analysis) summaryModel.summarize(analysis)
} }
} }
if (verbose) {
println("Rendering output")
}
renderSummary(projectRootFile, summaryModel, outDir) renderSummary(projectRootFile, summaryModel, outDir)
// Render summary // Render summary
System.err.println("Done, summary is in ${outDir}/index.html") System.err.println("Done, summary is in $outDir/index.html")
} }
} }

View File

@ -3,25 +3,26 @@ package me.msoucy.gbat
import java.io.File import java.io.File
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import me.msoucy.gbat.models.ChangeType import me.msoucy.gbat.models.ChangeType
import me.msoucy.gbat.models.Event import me.msoucy.gbat.models.Event
data class HistoryItem( data class HistoryItem(
val repoRoot : File, val repoRoot: File,
val projectRoot : File, val projectRoot: File,
val fname : File, val fname: File,
val authorDiffs : List<Pair<String, List<Event>>> val authorDiffs: List<Pair<String, List<Event>>>
) )
fun parseHistory(repo : GitRepo, fun parseHistory(
projectRoot : File, repo: GitRepo,
fname : File, projectRoot: File,
verbose : Boolean = false) : HistoryItem { fname: File,
verbose: Boolean = false
): HistoryItem {
val entries = repo.log(fname) val entries = repo.log(fname)
val repoRoot = repo.root() val repoRoot = repo.root()
if(verbose) { if (verbose) {
System.err.println("Parsing history for ${fname}") System.err.println("Parsing history for $fname")
} }
return HistoryItem(repoRoot, projectRoot, fname, return HistoryItem(repoRoot, projectRoot, fname,
entries.map { (author, diff) -> entries.map { (author, diff) ->
@ -30,27 +31,27 @@ fun parseHistory(repo : GitRepo,
) )
} }
fun diffWalk(diff : Diff) : List<Event> { fun diffWalk(diff: Diff): List<Event> {
fun String.startsChunk() = startsWith("@@") fun String.startsChunk() = startsWith("@@")
fun String.isOldLine() = startsWith("-") fun String.isOldLine() = startsWith("-")
fun String.isNewLine() = startsWith("+") fun String.isNewLine() = startsWith("+")
fun chunkify() : List<List<String>> { fun chunkify(): List<List<String>> {
val chunks = mutableListOf<MutableList<String>>() val chunks = mutableListOf<MutableList<String>>()
var curChunk = mutableListOf<String>() var curChunk = mutableListOf<String>()
diff.split("\n").forEach { line -> diff.split("\n").forEach { line ->
if(line.startsChunk()) { if (line.startsChunk()) {
if(curChunk.isNotEmpty()) { if (curChunk.isNotEmpty()) {
chunks.add(curChunk) chunks.add(curChunk)
curChunk = mutableListOf<String>() curChunk = mutableListOf<String>()
} }
curChunk.add(line) curChunk.add(line)
} else if(curChunk.isNotEmpty()) { } else if (curChunk.isNotEmpty()) {
curChunk.add(line) curChunk.add(line)
} }
} }
if(curChunk.isNotEmpty()) { if (curChunk.isNotEmpty()) {
chunks.add(curChunk) chunks.add(curChunk)
} }
return chunks return chunks
@ -60,23 +61,23 @@ fun diffWalk(diff : Diff) : List<Event> {
val events = mutableListOf<Event>() val events = mutableListOf<Event>()
class Hunk( class Hunk(
val lineNum : Int, val lineNum: Int,
val oldLines : List<String>, val oldLines: List<String>,
val newLines : List<String> val newLines: List<String>
) )
fun hunkize(chunkWoHeader : List<String>, firstLineNum : Int) : List<Hunk> { fun hunkize(chunkWoHeader: List<String>, firstLineNum: Int): List<Hunk> {
var curOld = mutableListOf<String>() var curOld = mutableListOf<String>()
var curNew = mutableListOf<String>() var curNew = mutableListOf<String>()
var curLine = firstLineNum var curLine = firstLineNum
var hunks = mutableListOf<Hunk>() var hunks = mutableListOf<Hunk>()
chunkWoHeader.forEach { line -> chunkWoHeader.forEach { line ->
if(line.isOldLine()) { if (line.isOldLine()) {
curOld.add(line) curOld.add(line)
} else if(line.isNewLine()) { } else if (line.isNewLine()) {
curNew.add(line) curNew.add(line)
} else if(curOld.isNotEmpty() || curNew.isNotEmpty()) { } else if (curOld.isNotEmpty() || curNew.isNotEmpty()) {
hunks.add(Hunk(curLine, curOld, curNew)) hunks.add(Hunk(curLine, curOld, curNew))
curLine += curNew.size + 1 curLine += curNew.size + 1
curOld = mutableListOf<String>() curOld = mutableListOf<String>()
@ -85,28 +86,28 @@ fun diffWalk(diff : Diff) : List<Event> {
curLine++ curLine++
} }
} }
if(curOld.isNotEmpty() || curNew.isNotEmpty()) { if (curOld.isNotEmpty() || curNew.isNotEmpty()) {
hunks.add(Hunk(curLine, curOld, curNew)) hunks.add(Hunk(curLine, curOld, curNew))
} }
return hunks return hunks
} }
fun stepHunk(hunk : Hunk) { fun stepHunk(hunk: Hunk) {
val oldLen = hunk.oldLines.size val oldLen = hunk.oldLines.size
val newLen = hunk.newLines.size val newLen = hunk.newLines.size
val maxLen = max(oldLen, newLen) val maxLen = max(oldLen, newLen)
var lineNum = hunk.lineNum var lineNum = hunk.lineNum
for (i in 0 until maxLen) { for (i in 0 until maxLen) {
if(i < oldLen && i < newLen) { if (i < oldLen && i < newLen) {
events += Event( events += Event(
ChangeType.Change, ChangeType.Change,
lineNum, lineNum,
hunk.newLines[i].substring(1) hunk.newLines[i].substring(1)
) )
lineNum++ lineNum++
} else if(i < oldLen) { } else if (i < oldLen) {
events += Event( events += Event(
ChangeType.Remove, ChangeType.Remove,
lineNum, lineNum,
@ -123,7 +124,7 @@ fun diffWalk(diff : Diff) : List<Event> {
} }
} }
fun stepChunk(chunk : List<String>) { fun stepChunk(chunk: List<String>) {
val header = chunk[0] val header = chunk[0]
// format of header is // format of header is
@ -137,7 +138,7 @@ fun diffWalk(diff : Diff) : List<Event> {
// of the file the new and old are the same, and since we add // 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 // and subtract lines as we go, we should stay in step with the
// new offsets. // new offsets.
val newOffset = offsets[1].split(",").map{ val newOffset = offsets[1].split(",").map {
abs(it.toInt()) abs(it.toInt())
}.first() }.first()
@ -150,4 +151,4 @@ fun diffWalk(diff : Diff) : List<Event> {
chunks.forEach(::stepChunk) chunks.forEach(::stepChunk)
return events return events
} }

View File

@ -1,40 +1,36 @@
package me.msoucy.gbat package me.msoucy.gbat
import java.io.File
import me.msoucy.gbat.models.ProjectTreeNode
import me.msoucy.gbat.models.ProjectTreeResult
import me.msoucy.gbat.models.Statistics
import me.msoucy.gbat.models.SummaryModel
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import java.io.File
import me.msoucy.gbat.models.SummaryModel
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.python.util.PythonInterpreter
val NUM_RISKIEST_AUTHORS = 10 val NUM_RISKIEST_AUTHORS = 10
val NUM_RISKIEST_FILES = 10 val NUM_RISKIEST_FILES = 10
class SummaryRenderer( class SummaryRenderer(
val summaryModel : SummaryModel, val summaryModel: SummaryModel,
val outputDir : File val outputDir: File
) { ) {
private val filesDir = File(outputDir, "files") private val filesDir = File(outputDir, "files")
private val gson = GsonBuilder().setPrettyPrinting().create() private val gson = GsonBuilder().setPrettyPrinting().create()
fun renderAll(projectRoot : File) { fun renderAll(projectRoot: File) {
createFilesDir() createFilesDir()
renderSummaryJson(projectRoot) renderSummaryJson(projectRoot)
renderFileJson(projectRoot) renderFileJson(projectRoot)
// renderSrc(projectRoot) renderSrc(projectRoot)
} }
private fun renderSummaryJson(projectRoot : File) { private fun renderSummaryJson(projectRoot: File) {
val summary = summaryModel.projectSummary(projectRoot.absolutePath) val summary = summaryModel.projectSummary(projectRoot.absolutePath)
val json = gson.toJson(summary) val json = gson.toJson(summary)
File(filesDir, "summary.json").writeText(json) File(filesDir, "summary.json").writeText(json)
} }
private fun renderFileJson(projectRoot : File) { private fun renderFileJson(projectRoot: File) {
summaryModel.projectFiles(projectRoot.absolutePath).forEach { summaryModel.projectFiles(projectRoot.absolutePath).forEach {
val json = gson.toJson(summaryModel.fileSummary(it.fileId)) val json = gson.toJson(summaryModel.fileSummary(it.fileId))
File(filesDir, "${it.fileId}.json").writeText(json) File(filesDir, "${it.fileId}.json").writeText(json)
@ -42,15 +38,41 @@ class SummaryRenderer(
} }
private fun createFilesDir() = filesDir.mkdirs() private fun createFilesDir() = filesDir.mkdirs()
private fun renderSrc(projectRoot: File) {
val interpreter = PythonInterpreter()
val cssFile = File(filesDir, "pygments.css")
interpreter.exec("""
from pygments.formatters import HtmlFormatter
formatter = HtmlFormatter(linenos=True, lineanchors='gbab')
formatCss = formatter.get_style_defs()
""")
cssFile.writeText(interpreter.get("formatCss", String::class.java))
summaryModel.projectFiles(projectRoot.absolutePath).forEach {
val resultFile = File(filesDir, "${it.fileId}.html")
val lines = summaryModel.fileLines(it.fileId)
val body = lines.joinToString("\n")
interpreter["fname"] = it.fname.toString()
interpreter["body"] = body
interpreter.exec("""
from pygments import highlight
from pygments.lexers import guess_lexer_for_filename
lexer = guess_lexer_for_filename(fname, body)
html = highlight(body, lexer, formatter)
""")
resultFile.writeText("""<link rel=stylesheet type="text/css" href="pygments.css">""" + interpreter.get("html", String::class.java))
}
}
} }
fun renderSummary( fun renderSummary(
projectRoot : File, projectRoot: File,
summaryModel : SummaryModel, summaryModel: SummaryModel,
outputDir : File outputDir: File
) { ) {
transaction(summaryModel.db) { transaction(summaryModel.db) {
val renderer = SummaryRenderer(summaryModel, outputDir) val renderer = SummaryRenderer(summaryModel, outputDir)
renderer.renderAll(projectRoot) renderer.renderAll(projectRoot)
} }
} }

View File

@ -5,8 +5,8 @@ import java.io.IOException
typealias Diff = String typealias Diff = String
class GitRepo(val projectRoot : File, val git_exe : String) { class GitRepo(val projectRoot: File, val git_exe: String) {
fun ls() : String { fun ls(): String {
val cmd = listOf( val cmd = listOf(
git_exe, git_exe,
"ls-tree", "ls-tree",
@ -14,13 +14,14 @@ class GitRepo(val projectRoot : File, val git_exe : String) {
"--name-only", "--name-only",
"-r", "-r",
"HEAD", "HEAD",
"--",
projectRoot.absolutePath projectRoot.absolutePath
) )
val (out, _) = cmd.runCommand(projectRoot) val (out, _) = cmd.runCommand(projectRoot)
return out ?: "" return out ?: ""
} }
fun root() : File { fun root(): File {
val cmd = listOf( val cmd = listOf(
git_exe, git_exe,
"rev-parse", "rev-parse",
@ -30,7 +31,7 @@ class GitRepo(val projectRoot : File, val git_exe : String) {
return File((out ?: "").trim()) return File((out ?: "").trim())
} }
fun log(fname : File) : List<Pair<String, Diff>> { fun log(fname: File): List<Pair<String, Diff>> {
val cmd = listOf( val cmd = listOf(
git_exe, git_exe,
"--no-pager", "--no-pager",
@ -40,14 +41,15 @@ class GitRepo(val projectRoot : File, val git_exe : String) {
"--follow", // Follow history through renames "--follow", // Follow history through renames
"--patience", // Use the patience diff algorithm "--patience", // Use the patience diff algorithm
"-p", // Show patches "-p", // Show patches
"--",
fname.absolutePath fname.absolutePath
) )
val (out, err) = cmd.runCommand(projectRoot) val (out, err) = cmd.runCommand(projectRoot)
if(err != "") { if (err != "") {
System.err.println("Error from git log: " + err) System.err.println("Error from git log: " + err)
throw IOException(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 { return logEntries.map {
val (header, diffLines) = splitEntryHeader(it) val (header, diffLines) = splitEntryHeader(it)
val diff = diffLines.joinToString("\n") val diff = diffLines.joinToString("\n")
@ -58,25 +60,25 @@ class GitRepo(val projectRoot : File, val git_exe : String) {
}.reversed() }.reversed()
} }
private fun parseAuthor(header : List<String>) : String { private fun parseAuthor(header: List<String>): String {
val segs = header.getOrNull(1)?.trim()?.split("\\s+".toRegex())?: listOf() val segs = header.getOrNull(1)?.trim()?.split("\\s+".toRegex()) ?: listOf()
return segs.subList(1, segs.size - 1).joinToString(" ") return segs.subList(1, segs.size - 1).joinToString(" ")
} }
private fun splitEntryHeader(entry : String) : Pair<List<String>, List<String>> { private fun splitEntryHeader(entry: String): Pair<List<String>, List<String>> {
val lines = entry.split("\n") val lines = entry.split("\n")
if(lines.size < 2) { if (lines.size < 2) {
return Pair(listOf(), listOf()) return Pair(listOf(), listOf())
} else if(!lines.get(0).startsWith("commit")) { } else if (!lines.get(0).startsWith("commit")) {
return Pair(listOf(), listOf()) return Pair(listOf(), listOf())
} else if(!lines.get(1).startsWith("Author")) { } else if (!lines.get(1).startsWith("Author")) {
return Pair(listOf(), listOf()) return Pair(listOf(), listOf())
} }
var ind = 2 var ind = 2
while(ind < lines.size && !lines.get(ind).startsWith("diff")) { while (ind < lines.size && !lines.get(ind).startsWith("diff")) {
ind++ ind++
} }
return Pair(lines.subList(0, ind).copyOf(), return Pair(lines.subList(0, ind).copyOf(),
lines.subList(ind, lines.size).copyOf()) lines.subList(ind, lines.size).copyOf())
} }
} }

View File

@ -4,10 +4,10 @@ import java.io.File
import java.io.IOException import java.io.IOException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
fun <T> Iterable<T>.copyOf() : List<T> = mutableListOf<T>().also { it.addAll(this) } fun <T> Iterable<T>.copyOf(): List<T> = mutableListOf<T>().also { it.addAll(this) }
fun <T> Iterable<T>.mutableCopyOf() = mutableListOf<T>().also { it.addAll(this) } fun <T> Iterable<T>.mutableCopyOf() = mutableListOf<T>().also { it.addAll(this) }
fun List<String>.runCommand(workingDir: File): Pair<String?,String?> { fun List<String>.runCommand(workingDir: File): Pair<String?, String?> {
try { try {
val proc = ProcessBuilder(*this.toTypedArray()) val proc = ProcessBuilder(*this.toTypedArray())
.directory(workingDir) .directory(workingDir)
@ -18,8 +18,8 @@ fun List<String>.runCommand(workingDir: File): Pair<String?,String?> {
proc.waitFor(5, TimeUnit.SECONDS) proc.waitFor(5, TimeUnit.SECONDS)
return Pair(proc.inputStream.bufferedReader().readText(), return Pair(proc.inputStream.bufferedReader().readText(),
proc.errorStream.bufferedReader().readText()) proc.errorStream.bufferedReader().readText())
} catch(e: IOException) { } catch (e: IOException) {
e.printStackTrace() e.printStackTrace()
return Pair(null, null) return Pair(null, null)
} }
} }

View File

@ -1,30 +1,25 @@
package me.msoucy.gbat.models package me.msoucy.gbat.models
import java.io.File
import java.nio.file.Path
import java.nio.file.Paths
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.copyOf
import me.msoucy.gbat.mutableCopyOf 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 org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
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, class KnowledgeAcct(
var authors : List<String>, var knowledgeAcctId: Int,
var authorsStr : String) var authors: List<String>,
var authorsStr: String
)
object AuthorsTable : Table("authors") { object AuthorsTable : IntIdTable("authors", "authorid") {
val id = integer("authorid")
val author = text("author").uniqueIndex("authors_idx") val author = text("author").uniqueIndex("authors_idx")
override val primaryKey = PrimaryKey(id)
} }
object KnowledgeAcctsTable : Table("knowledgeaccts") { object KnowledgeAcctsTable : IntIdTable("knowledgeaccts", "knowledgeacctid") {
val id = integer("knowledgeacctid")
val authors = text("authors").uniqueIndex("knowledgeacctsauthors_idx") val authors = text("authors").uniqueIndex("knowledgeacctsauthors_idx")
override val primaryKey = PrimaryKey(id)
} }
object KnowledgeAuthorsTable : Table("knowedgeaccts_authors") { object KnowledgeAuthorsTable : Table("knowedgeaccts_authors") {
val knowledgeacctid = integer("knowledgeacctid") val knowledgeacctid = integer("knowledgeacctid")
@ -33,7 +28,7 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R
} }
object LineKnowledge : Table("lineknowledge") { object LineKnowledge : Table("lineknowledge") {
val linenum = integer("linenum") val linenum = integer("linenum")
val knowledgeacctid = integer("knowledgeacctid") val knowledgeacctid = integer("knowledgeacctid").references(KnowledgeAuthorsTable.knowledgeacctid)
val knowledge = double("knowledge") val knowledge = double("knowledge")
} }
@ -45,13 +40,13 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R
val SAFE_KNOWLEDGE_ACCT_ID = 1 val SAFE_KNOWLEDGE_ACCT_ID = 1
val KNOWLEDGE_PER_LINE_ADDED = 1000.0 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.Add -> lineAdded(author, lineNum)
ChangeType.Change -> lineChanged(author, lineNum) ChangeType.Change -> lineChanged(author, lineNum)
ChangeType.Remove -> lineRemoved(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 kCreated = constant * KNOWLEDGE_PER_LINE_ADDED
val kAcquired = (1 - constant) * KNOWLEDGE_PER_LINE_ADDED val kAcquired = (1 - constant) * KNOWLEDGE_PER_LINE_ADDED
val totLineK = totalLineKnowledge(lineNum) val totLineK = totalLineKnowledge(lineNum)
@ -63,21 +58,21 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R
adjustKnowledge(knowledgeAcctId, lineNum, kCreated) adjustKnowledge(knowledgeAcctId, lineNum, kCreated)
} }
fun lineRemoved(lineNum : Int) { fun lineRemoved(lineNum: Int) {
allAcctsWithKnowledgeOf(lineNum).forEach { allAcctsWithKnowledgeOf(lineNum).forEach {
destroyLineKnowledge(it, lineNum) destroyLineKnowledge(it, lineNum)
} }
bumpAllLinesFrom(lineNum, -1) bumpAllLinesFrom(lineNum, -1)
} }
fun lineAdded(author : String, lineNum : Int) { fun lineAdded(author: String, lineNum: Int) {
val knowledgeAcctId = lookupOrCreateKnowledgeAcct(listOf(author)) val knowledgeAcctId = lookupOrCreateKnowledgeAcct(listOf(author))
bumpAllLinesFrom(lineNum-1, 1) bumpAllLinesFrom(lineNum - 1, 1)
adjustKnowledge(knowledgeAcctId, lineNum, KNOWLEDGE_PER_LINE_ADDED) adjustKnowledge(knowledgeAcctId, lineNum, KNOWLEDGE_PER_LINE_ADDED)
} }
fun knowledgeSummary(lineNum : Int) = transaction(db) { fun knowledgeSummary(lineNum: Int) = transaction(db) {
LineKnowledge.select { LineKnowledge.selectAll().where {
LineKnowledge.linenum eq lineNum LineKnowledge.linenum eq lineNum
}.map { }.map {
val acct = getKnowledgeAcct(it[LineKnowledge.knowledgeacctid]) val acct = getKnowledgeAcct(it[LineKnowledge.knowledgeacctid])
@ -87,35 +82,35 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R
}.copyOf() }.copyOf()
} }
private fun bumpAllLinesFrom(lineNum : Int, adjustment : Int) = transaction(db) { private fun bumpAllLinesFrom(lineNum: Int, adjustment: Int) = transaction(db) {
LineKnowledge.update({LineKnowledge.linenum greater lineNum}) { LineKnowledge.update({ LineKnowledge.linenum greater lineNum }) {
with(SqlExpressionBuilder) { with(SqlExpressionBuilder) {
it[LineKnowledge.linenum] = LineKnowledge.linenum + adjustment 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.selectAll().where {
KnowledgeAcctsTable.id eq knowledgeAcctId KnowledgeAcctsTable.id eq knowledgeAcctId
}.map { }.map {
KnowledgeAcct( KnowledgeAcct(
it[KnowledgeAcctsTable.id], it[KnowledgeAcctsTable.id].value,
it[KnowledgeAcctsTable.authors].split("\n"), it[KnowledgeAcctsTable.authors].split("\n"),
it[KnowledgeAcctsTable.authors] it[KnowledgeAcctsTable.authors]
) )
}.firstOrNull() ?: KnowledgeAcct(-1, listOf(), "") }.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.deleteWhere {
(LineKnowledge.knowledgeacctid eq knowledgeId) and (knowledgeacctid eq knowledgeId) and
(LineKnowledge.linenum eq lineNum) (linenum eq lineNum)
} }
} }
private fun redistributeKnowledge(author : String, lineNum : Int, redistPct : Double) { private fun redistributeKnowledge(author: String, lineNum: Int, redistPct: Double) {
if(riskModel.isDeparted(author)) { if (riskModel.isDeparted(author)) {
return return
} }
val knowledgeIds = nonSafeAcctsWithKnowledgeOf(lineNum) val knowledgeIds = nonSafeAcctsWithKnowledgeOf(lineNum)
@ -124,13 +119,13 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R
if (author !in knowledgeAcct.authors) { if (author !in knowledgeAcct.authors) {
val oldKnowledge = knowledgeInAcct(knowledgeAcct.knowledgeAcctId, lineNum) val oldKnowledge = knowledgeInAcct(knowledgeAcct.knowledgeAcctId, lineNum)
var newAuthors = knowledgeAcct.authors.mutableCopyOf() var newAuthors = knowledgeAcct.authors.mutableCopyOf()
if(newAuthors.all(riskModel::isDeparted)) { if (newAuthors.all(riskModel::isDeparted)) {
newAuthors = mutableListOf(author) newAuthors = mutableListOf(author)
} else { } else {
newAuthors.add(author) newAuthors.add(author)
} }
newAuthors = newAuthors.sorted().mutableCopyOf() newAuthors = newAuthors.sorted().mutableCopyOf()
val newKnowledgeId = if(riskModel.jointBusProbBelowThreshold(newAuthors)) { val newKnowledgeId = if (riskModel.jointBusProbBelowThreshold(newAuthors)) {
SAFE_KNOWLEDGE_ACCT_ID SAFE_KNOWLEDGE_ACCT_ID
} else { } else {
lookupOrCreateKnowledgeAcct(newAuthors) lookupOrCreateKnowledgeAcct(newAuthors)
@ -142,8 +137,8 @@ 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.selectAll().where {
(LineKnowledge.knowledgeacctid eq knowledgeAcctId) and (LineKnowledge.knowledgeacctid eq knowledgeAcctId) and
(LineKnowledge.linenum eq lineNum) (LineKnowledge.linenum eq lineNum)
}.map { }.map {
@ -151,8 +146,8 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R
}.first() }.first()
} }
private fun nonSafeAcctsWithKnowledgeOf(lineNum : Int) = transaction(db) { private fun nonSafeAcctsWithKnowledgeOf(lineNum: Int) = transaction(db) {
LineKnowledge.select { LineKnowledge.selectAll().where {
(LineKnowledge.linenum eq lineNum) and (LineKnowledge.linenum eq lineNum) and
(LineKnowledge.knowledgeacctid neq SAFE_KNOWLEDGE_ACCT_ID) (LineKnowledge.knowledgeacctid neq SAFE_KNOWLEDGE_ACCT_ID)
}.map { }.map {
@ -160,20 +155,20 @@ 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.selectAll().where {
LineKnowledge.linenum eq lineNum LineKnowledge.linenum eq lineNum
}.map { }.map {
it[LineKnowledge.knowledgeacctid] it[LineKnowledge.knowledgeacctid]
} }
} }
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 { val lineExists = LineKnowledge.selectAll().where {
(LineKnowledge.knowledgeacctid eq knowledgeAcctId) and (LineKnowledge.knowledgeacctid eq knowledgeAcctId) and
(LineKnowledge.linenum eq lineNum) (LineKnowledge.linenum eq lineNum)
}.count() > 0 }.count() > 0
if(!lineExists) { if (!lineExists) {
LineKnowledge.insert { LineKnowledge.insert {
it[LineKnowledge.knowledgeacctid] = knowledgeAcctId it[LineKnowledge.knowledgeacctid] = knowledgeAcctId
it[LineKnowledge.linenum] = lineNum it[LineKnowledge.linenum] = lineNum
@ -183,53 +178,52 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R
LineKnowledge.update({ LineKnowledge.update({
(LineKnowledge.knowledgeacctid eq knowledgeAcctId) and (LineKnowledge.knowledgeacctid eq knowledgeAcctId) and
(LineKnowledge.linenum eq lineNum) (LineKnowledge.linenum eq lineNum)
}) { }) {
with(SqlExpressionBuilder) { with(SqlExpressionBuilder) {
it[LineKnowledge.knowledge] = LineKnowledge.knowledge + adjustment it[LineKnowledge.knowledge] = LineKnowledge.knowledge + adjustment
} }
} }
} }
private fun lookupOrCreateKnowledgeAcct(authors : List<String>) = transaction(db) { private fun lookupOrCreateKnowledgeAcct(authors: List<String>) = transaction(db) {
val authorStr = authors.sorted().joinToString("\n") val authorStr = authors.sorted().joinToString("\n")
var newId = KnowledgeAcctsTable.select { KnowledgeAcctsTable.selectAll().where {
KnowledgeAcctsTable.authors eq authorStr KnowledgeAcctsTable.authors eq authorStr
}.map { }.map {
it[KnowledgeAcctsTable.id] it[KnowledgeAcctsTable.id].value
}.firstOrNull() ?: -1 }.firstOrNull() ?: run {
if (newId != -1) { KnowledgeAcctsTable.insert {
KnowledgeAcctsTable.insert {
it[KnowledgeAcctsTable.authors] = authorStr it[KnowledgeAcctsTable.authors] = authorStr
} }
newId = KnowledgeAcctsTable.select { val theNewId = KnowledgeAcctsTable.selectAll().where {
KnowledgeAcctsTable.authors eq authorStr KnowledgeAcctsTable.authors eq authorStr
}.map { }.map {
it[KnowledgeAcctsTable.id] it[KnowledgeAcctsTable.id].value
}.first() }.first()
authors.map(::lookupOrCreateAuthor).forEach { authorId -> authors.map(::lookupOrCreateAuthor).forEach { authorId ->
KnowledgeAuthorsTable.insert { KnowledgeAuthorsTable.insert {
it[KnowledgeAuthorsTable.knowledgeacctid] = newId it[KnowledgeAuthorsTable.knowledgeacctid] = theNewId
it[KnowledgeAuthorsTable.authorid] = authorId it[KnowledgeAuthorsTable.authorid] = authorId.value
} }
} }
theNewId
} }
newId
} }
private fun lookupOrCreateAuthor(authorName : String) = transaction(db) { private fun lookupOrCreateAuthor(authorName: String) = transaction(db) {
AuthorsTable.insertIgnore { AuthorsTable.insertIgnore {
it[author] = authorName it[author] = authorName
} }
AuthorsTable.select { AuthorsTable.selectAll().where {
AuthorsTable.author eq authorName AuthorsTable.author eq authorName
}.first().let { }.first().let {
it[AuthorsTable.id] it[AuthorsTable.id]
} }
} }
private fun totalLineKnowledge(linenum : Int) = transaction(db) { private fun totalLineKnowledge(linenum: Int) = transaction(db) {
LineKnowledge.select { LineKnowledge.selectAll().where {
LineKnowledge.linenum eq linenum LineKnowledge.linenum eq linenum
}.map { }.map {
it[LineKnowledge.knowledge] it[LineKnowledge.knowledge]
@ -239,12 +233,10 @@ class KnowledgeModel(val db : Database, val constant : Double, val riskModel : R
private fun createTables() = transaction(db) { private fun createTables() = transaction(db) {
SchemaUtils.dropDatabase() SchemaUtils.dropDatabase()
SchemaUtils.createMissingTablesAndColumns(AuthorsTable, KnowledgeAcctsTable, KnowledgeAuthorsTable, LineKnowledge) SchemaUtils.createMissingTablesAndColumns(AuthorsTable, KnowledgeAcctsTable, KnowledgeAuthorsTable, LineKnowledge)
AuthorsTable.insertIgnore { AuthorsTable.insertIgnore {
it[id] = 1
it[author] = "" it[author] = ""
} }
KnowledgeAcctsTable.insertIgnore { KnowledgeAcctsTable.insertIgnore {
it[id] = 1
it[authors] = "" it[authors] = ""
} }
KnowledgeAuthorsTable.insertIgnore { KnowledgeAuthorsTable.insertIgnore {

View File

@ -1,42 +1,34 @@
package me.msoucy.gbat.models 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() { class LineModel() {
inner class Line(var num : Int, var text : String) inner class Line(var num: Int, var text: String)
val model = mutableSetOf<Line>() val model = mutableSetOf<Line>()
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.Add -> add(Line(lineNum, lineText))
ChangeType.Change -> change(Line(lineNum, lineText)) ChangeType.Change -> change(Line(lineNum, lineText))
ChangeType.Remove -> del(Line(lineNum, lineText)) ChangeType.Remove -> del(Line(lineNum, lineText))
} }
fun add(line : Line) { fun add(line: Line) {
model.onEach { entry -> model.onEach { entry ->
if(entry.num >= line.num) { if (entry.num >= line.num) {
entry.num++ entry.num++
} }
} }
model.add(line) model.add(line)
} }
fun del(line : Line) { fun del(line: Line) {
model.removeIf { it.num == line.num } model.removeIf { it.num == line.num }
model.onEach { entry -> model.onEach { entry ->
if(entry.num > line.num) { if (entry.num > line.num) {
entry.num-- entry.num--
} }
} }
} }
fun change(line : Line) { fun change(line: Line) {
model.removeIf { it.num == line.num } model.removeIf { it.num == line.num }
model.add(line) model.add(line)
} }

View File

@ -7,37 +7,37 @@ enum class ChangeType {
} }
data class Event( data class Event(
val eventType : ChangeType, val eventType: ChangeType,
val lineNum : Int, val lineNum: Int,
val lineVal : String? val lineVal: String?
) )
data class Condensation( data class Condensation(
val authors : List<String>, val authors: List<String>,
val knowledge : Double, val knowledge: Double,
val orphaned : Double, val orphaned: Double,
val risk : Double = 0.0 val risk: Double = 0.0
) : Comparable<Condensation> { ) : Comparable<Condensation> {
override operator fun compareTo(other : Condensation) : Int { override operator fun compareTo(other: Condensation): Int {
var result = authors.size.compareTo(other.authors.size) var result = authors.size.compareTo(other.authors.size)
if(result == 0) { if (result == 0) {
authors.zip(other.authors).forEach { (a, b) -> 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) result = knowledge.compareTo(other.knowledge)
if(result == 0) if (result == 0)
result = orphaned.compareTo(other.orphaned) result = orphaned.compareTo(other.orphaned)
if(result == 0) if (result == 0)
result = risk.compareTo(other.risk) result = risk.compareTo(other.risk)
return result return result
} }
} }
class CondensedAnalysis( class CondensedAnalysis(
var repoRoot : File, var repoRoot: File,
var projectRoot : File, var projectRoot: File,
var fileName : File, var fileName: File,
var lineSummaries : MutableList<Pair<String, List<Condensation>>> = mutableListOf() var lineSummaries: MutableList<Pair<String, List<Condensation>>> = mutableListOf()
) )

View File

@ -3,50 +3,52 @@ package me.msoucy.gbat.models
import java.io.File import java.io.File
import kotlin.io.forEachLine import kotlin.io.forEachLine
class RiskModel(val threshold : Double, class RiskModel(
val default : Double, val threshold: Double,
val busRiskFile : File?, val default: Double,
val departedFile : File?) { val busRiskFile: File?,
val departedFile: File?
) {
val departed = mutableSetOf<String>() val departed = mutableSetOf<String>()
val risks = mutableMapOf<String, Double>().withDefault {default} val risks = mutableMapOf<String, Double>().withDefault { default }
init { init {
parseBusRisks() parseBusRisks()
parseDeparted() parseDeparted()
} }
operator fun get(author : String) : Double { operator fun get(author: String): Double {
val name = author.trim() val name = author.trim()
if(name.isEmpty()) { if (name.isEmpty()) {
return threshold return threshold
} }
return risks.getOrPut(name) { default } 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<String>) = fun jointBusProb(authors: List<String>) =
(authors.map { this[it] } + 1.0).reduce { a, b -> a * b } (authors.map { this[it] } + 1.0).reduce { a, b -> a * b }
fun jointBusProbBelowThreshold(authors : List<String>) = fun jointBusProbBelowThreshold(authors: List<String>) =
jointBusProb(authors) <= threshold jointBusProb(authors) <= threshold
private fun parseBusRisks() { private fun parseBusRisks() {
busRiskFile?.forEachLine { line -> busRiskFile?.forEachLine { line ->
val sline = line.trim() val sline = line.trim()
if(sline.isNotEmpty()) { if (sline.isNotEmpty()) {
val segments = sline.split("=") val segments = sline.split("=")
val risk = segments.last() val risk = segments.last()
val author = segments.dropLast(1).joinToString(separator="=") val author = segments.dropLast(1).joinToString(separator = "=")
risks[author] = risk.toDouble() risks[author] = risk.toDouble()
} }
} }
} }
private fun parseDeparted() { private fun parseDeparted() {
departedFile?.forEachLine { line -> departedFile?.forEachLine { line ->
val author = line.trim() val author = line.trim()
if(author.isNotEmpty()) { if (author.isNotEmpty()) {
risks[author] = 1.0 risks[author] = 1.0
departed.add(author) departed.add(author)
} }

View File

@ -55,7 +55,7 @@ class FileTree {
var authorRisks = mutableMapOf<String, Statistics>() var authorRisks = mutableMapOf<String, Statistics>()
var lines = mutableListOf<LineDict>() var lines = mutableListOf<LineDict>()
} }
class FileEntry(var name : String = "") { class FileEntry(var name: String = "") {
var stats = Statistics() var stats = Statistics()
var authorRisks = mutableMapOf<String, Statistics>() var authorRisks = mutableMapOf<String, Statistics>()
} }
@ -64,14 +64,14 @@ class ProjectTree {
var files = mutableMapOf<Int, FileEntry>() var files = mutableMapOf<Int, FileEntry>()
var dirs = mutableListOf<Int>() var dirs = mutableListOf<Int>()
} }
class ProjectFilesResult(var fileId : Int, var fname : Path) class ProjectFilesResult(var fileId: Int, var fname: Path)
data class Statistics( data class Statistics(
var totKnowledge : Double = 0.0, var totKnowledge: Double = 0.0,
var totRisk : Double = 0.0, var totRisk: Double = 0.0,
var totOrphaned : Double = 0.0 var totOrphaned: Double = 0.0
) { ) {
constructor(row : ResultRow) : constructor(row: ResultRow) :
this(row[AllocationsTable.knowledge.sum()] ?: 0.0, this(row[AllocationsTable.knowledge.sum()] ?: 0.0,
row[AllocationsTable.risk.sum()] ?: 0.0, row[AllocationsTable.risk.sum()] ?: 0.0,
row[AllocationsTable.orphaned.sum()] ?: 0.0) {} row[AllocationsTable.orphaned.sum()] ?: 0.0) {}
@ -81,25 +81,25 @@ class ProjectTreeNode {
var files = mutableListOf<FileEntry>() var files = mutableListOf<FileEntry>()
var dirs = mutableListOf<ProjectTreeNode>() var dirs = mutableListOf<ProjectTreeNode>()
} }
class ProjectTreeResult(var name : String, var root : ProjectTreeNode) { class ProjectTreeResult(var name: String, var root: ProjectTreeNode) {
var stats = Statistics() var stats = Statistics()
var authorRisks = mutableMapOf<String, Statistics>() var authorRisks = mutableMapOf<String, Statistics>()
} }
class SummaryModel(val db : Database) { class SummaryModel(val db: Database) {
val GIT_BY_A_BUS_BELOW_THRESHOLD = "Git by a Bus Safe Author" val GIT_BY_A_BUS_BELOW_THRESHOLD = "Git by a Bus Safe Author"
init { init {
createTables() createTables()
} }
private val lineAllocations = (LinesTable leftJoin AllocationsTable) private val lineAllocations = (LinesTable leftJoin AllocationsTable)
private val lineAllocationGroups = (lineAllocations leftJoin AuthorsGroupsTable) private val lineAllocationGroups = (lineAllocations leftJoin AuthorsGroupsTable)
private val manyJoined = (lineAllocations leftJoin FilesTable leftJoin DirsTable) private val manyJoined = (lineAllocations leftJoin FilesTable leftJoin DirsTable)
private val allJoined = (manyJoined leftJoin AuthorsGroupsTable) private val allJoined = (manyJoined leftJoin AuthorsGroupsTable)
fun summarize(ca : CondensedAnalysis) { fun summarize(ca: CondensedAnalysis) {
val fname = adjustFname(ca.repoRoot.absoluteFile, val fname = adjustFname(ca.repoRoot.absoluteFile,
ca.projectRoot.absoluteFile, ca.projectRoot.absoluteFile,
ca.fileName.absoluteFile) ca.fileName.absoluteFile)
@ -122,7 +122,7 @@ class SummaryModel(val db : Database) {
} }
} }
} }
fun totalKnowledge() = transaction(db) { fun totalKnowledge() = transaction(db) {
AllocationsTable.selectAll().map { it[AllocationsTable.knowledge] }.sum() AllocationsTable.selectAll().map { it[AllocationsTable.knowledge] }.sum()
} }
@ -139,12 +139,12 @@ class SummaryModel(val db : Database) {
FilesTable.selectAll().count() FilesTable.selectAll().count()
} }
fun authorgroupsWithRisk(top : Int? = null) : List<Pair<String, Double>> = transaction(db) { fun authorgroupsWithRisk(top: Int? = null): List<Pair<String, Double>> = transaction(db) {
var query = (AllocationsTable innerJoin AuthorsGroupsTable) var query = (AllocationsTable innerJoin AuthorsGroupsTable)
.selectAll() .selectAll()
.groupBy(AuthorsGroupsTable.authors) .groupBy(AuthorsGroupsTable.authors)
.orderBy(AllocationsTable.risk.sum() to SortOrder.DESC) .orderBy(AllocationsTable.risk.sum() to SortOrder.DESC)
if(top != null) { if (top != null) {
query = query.limit(top) query = query.limit(top)
} }
query.map { query.map {
@ -152,12 +152,12 @@ class SummaryModel(val db : Database) {
} }
} }
fun fileidsWithRisk(top : Int? = null) : List<Pair<Int, Double>> = transaction(db) { fun fileidsWithRisk(top: Int? = null): List<Pair<Int, Double>> = transaction(db) {
var query = (FilesTable leftJoin LinesTable leftJoin AllocationsTable) var query = (FilesTable leftJoin LinesTable leftJoin AllocationsTable)
.selectAll() .selectAll()
.groupBy(FilesTable.id) .groupBy(FilesTable.id)
.orderBy(AllocationsTable.risk.sum() to SortOrder.DESC) .orderBy(AllocationsTable.risk.sum() to SortOrder.DESC)
if(top != null) { if (top != null) {
query = query.limit(top) query = query.limit(top)
} }
query.map { query.map {
@ -165,8 +165,8 @@ class SummaryModel(val db : Database) {
} }
} }
fun fpath(fileId : Int) : Path = transaction(db) { fun fpath(fileId: Int): Path = transaction(db) {
FilesTable.select { FilesTable.selectAll().where {
FilesTable.id eq fileId FilesTable.id eq fileId
}.first().let { row -> }.first().let { row ->
val dirs = reconsDirs(row[FilesTable.dirid]) val dirs = reconsDirs(row[FilesTable.dirid])
@ -174,9 +174,9 @@ class SummaryModel(val db : Database) {
} }
} }
fun projectFiles(project : String) : List<ProjectFilesResult> = transaction(db) { fun projectFiles(project: String): List<ProjectFilesResult> = transaction(db) {
val projectId = findOrCreateProject(project) val projectId = findOrCreateProject(project)
(FilesTable innerJoin DirsTable).select { (FilesTable innerJoin DirsTable).selectAll().where {
(FilesTable.dirid eq DirsTable.id) and (FilesTable.dirid eq DirsTable.id) and
(DirsTable.projectid eq projectId) (DirsTable.projectid eq projectId)
}.map { row -> }.map { row ->
@ -184,15 +184,15 @@ class SummaryModel(val db : Database) {
} }
} }
fun projectSummary(project : String) = transaction(db) { fun projectSummary(project: String) = transaction(db) {
val projectId = findOrCreateProject(project) val projectId = findOrCreateProject(project)
val theTree = mutableMapOf<Int, ProjectTree>().withDefault {ProjectTree()} val theTree = mutableMapOf<Int, ProjectTree>().withDefault { ProjectTree() }
// First fill in the directory structure, ignoring the files // First fill in the directory structure, ignoring the files
val parentDirIds = mutableListOf(0) val parentDirIds = mutableListOf(0)
while(parentDirIds.isNotEmpty()) { while (parentDirIds.isNotEmpty()) {
val parentId = parentDirIds.removeAt(0) val parentId = parentDirIds.removeAt(0)
DirsTable.select { DirsTable.parentdirid eq parentId } DirsTable.selectAll().where { DirsTable.parentdirid eq parentId }
.forEach { row -> .forEach { row ->
val dirId = row[DirsTable.id].value val dirId = row[DirsTable.id].value
theTree.getOrPut(parentId) { ProjectTree() }.dirs.add(dirId) theTree.getOrPut(parentId) { ProjectTree() }.dirs.add(dirId)
@ -203,13 +203,13 @@ class SummaryModel(val db : Database) {
// Then add the files // Then add the files
theTree.entries.forEach { entry -> theTree.entries.forEach { entry ->
FilesTable.select { FilesTable.dirid eq entry.key }.forEach { row -> FilesTable.selectAll().where { FilesTable.dirid eq entry.key }.forEach { row ->
entry.value.files[row[FilesTable.id].value] = FileEntry(row[FilesTable.fname]) entry.value.files[row[FilesTable.id].value] = FileEntry(row[FilesTable.fname])
} }
entry.value.files.entries.forEach { (fileId, fileEntry) -> entry.value.files.entries.forEach { (fileId, fileEntry) ->
lineAllocations lineAllocations
.slice(AllocationsTable.knowledge.sum(), AllocationsTable.risk.sum(), AllocationsTable.orphaned.sum()) .select(AllocationsTable.knowledge.sum(), AllocationsTable.risk.sum(), AllocationsTable.orphaned.sum())
.select { LinesTable.fileid eq fileId } .where { LinesTable.fileid eq fileId }
.groupBy(LinesTable.fileid) .groupBy(LinesTable.fileid)
.forEach { row -> .forEach { row ->
fileEntry.stats.totKnowledge = row[AllocationsTable.knowledge.sum()] ?: 0.0 fileEntry.stats.totKnowledge = row[AllocationsTable.knowledge.sum()] ?: 0.0
@ -219,12 +219,12 @@ class SummaryModel(val db : Database) {
} }
entry.value.files.entries.forEach { (fileId, fileEntry) -> entry.value.files.entries.forEach { (fileId, fileEntry) ->
lineAllocationGroups lineAllocationGroups
.slice( .select(
AllocationsTable.knowledge.sum(), AllocationsTable.knowledge.sum(),
AllocationsTable.risk.sum(), AllocationsTable.risk.sum(),
AllocationsTable.orphaned.sum(), AllocationsTable.orphaned.sum(),
AuthorsGroupsTable.authors AuthorsGroupsTable.authors
).select { LinesTable.fileid eq fileId } ).where { LinesTable.fileid eq fileId }
.groupBy(AllocationsTable.authorgroupid) .groupBy(AllocationsTable.authorgroupid)
.orderBy(AuthorsGroupsTable.authors) .orderBy(AuthorsGroupsTable.authors)
.forEach { row -> .forEach { row ->
@ -239,24 +239,24 @@ class SummaryModel(val db : Database) {
val projectTree = ProjectTreeResult(project, root) val projectTree = ProjectTreeResult(project, root)
allJoined allJoined
.slice( .select(
AllocationsTable.knowledge.sum(), AllocationsTable.knowledge.sum(),
AllocationsTable.risk.sum(), AllocationsTable.risk.sum(),
AllocationsTable.orphaned.sum(), AllocationsTable.orphaned.sum(),
AuthorsGroupsTable.authors AuthorsGroupsTable.authors
) )
.select { DirsTable.projectid eq projectId } .where { DirsTable.projectid eq projectId }
.groupBy(AuthorsGroupsTable.id) .groupBy(AuthorsGroupsTable.id)
.forEach { row -> .forEach { row ->
projectTree.authorRisks[row[AuthorsGroupsTable.authors]] = Statistics(row) projectTree.authorRisks[row[AuthorsGroupsTable.authors]] = Statistics(row)
} }
manyJoined manyJoined
.slice( .select(
AllocationsTable.knowledge.sum(), AllocationsTable.knowledge.sum(),
AllocationsTable.risk.sum(), AllocationsTable.risk.sum(),
AllocationsTable.orphaned.sum() AllocationsTable.orphaned.sum()
).select { ).where {
DirsTable.projectid eq projectId DirsTable.projectid eq projectId
}.first().let { row -> }.first().let { row ->
projectTree.stats = Statistics(row) projectTree.stats = Statistics(row)
@ -265,11 +265,11 @@ class SummaryModel(val db : Database) {
projectTree projectTree
} }
fun fileSummary(fileId : Int) = transaction(db) { fun fileSummary(fileId: Int) = transaction(db) {
var fileTree = FileTree() var fileTree = FileTree()
lineAllocationGroups lineAllocationGroups
.slice(AllocationsTable.knowledge.sum(), AllocationsTable.risk.sum(), AllocationsTable.orphaned.sum(), AuthorsGroupsTable.authors) .select(AllocationsTable.knowledge.sum(), AllocationsTable.risk.sum(), AllocationsTable.orphaned.sum(), AuthorsGroupsTable.authors)
.select { .where {
LinesTable.fileid eq fileId LinesTable.fileid eq fileId
}.groupBy(AuthorsGroupsTable.id).forEach { row -> }.groupBy(AuthorsGroupsTable.id).forEach { row ->
val authors = row[AuthorsGroupsTable.authors] val authors = row[AuthorsGroupsTable.authors]
@ -279,29 +279,29 @@ class SummaryModel(val db : Database) {
JoinType.LEFT, JoinType.LEFT,
LinesTable.fileid, FilesTable.id LinesTable.fileid, FilesTable.id
) )
.slice(AllocationsTable.knowledge.sum(), AllocationsTable.risk.sum(), AllocationsTable.orphaned.sum()) .select(AllocationsTable.knowledge.sum(), AllocationsTable.risk.sum(), AllocationsTable.orphaned.sum())
.select { LinesTable.fileid eq fileId } .where { LinesTable.fileid eq fileId }
.first().let { row -> .first().let { row ->
fileTree.stats = Statistics(row) fileTree.stats = Statistics(row)
} }
fileTree.name = FilesTable.select { FilesTable.id eq fileId }.map { it[FilesTable.fname] }.first() fileTree.name = FilesTable.selectAll().where { FilesTable.id eq fileId }.map { it[FilesTable.fname] }.first()
LinesTable.select { LinesTable.fileid eq fileId } LinesTable.selectAll().where { LinesTable.fileid eq fileId }
.map { it[LinesTable.id].value } .map { it[LinesTable.id].value }
.forEach { lineId -> .forEach { lineId ->
val lineDict = LineDict() val lineDict = LineDict()
lineAllocationGroups lineAllocationGroups
.slice(AllocationsTable.knowledge.sum(), AllocationsTable.risk.sum(), AllocationsTable.orphaned.sum(), AuthorsGroupsTable.authors) .select(AllocationsTable.knowledge.sum(), AllocationsTable.risk.sum(), AllocationsTable.orphaned.sum(), AuthorsGroupsTable.authors)
.select { .where {
LinesTable.id eq lineId LinesTable.id eq lineId
}.groupBy(AuthorsGroupsTable.id).forEach { lineRow -> }.groupBy(AuthorsGroupsTable.id).forEach { lineRow ->
lineDict.authorRisks[lineRow[AuthorsGroupsTable.authors]] = Statistics(lineRow) lineDict.authorRisks[lineRow[AuthorsGroupsTable.authors]] = Statistics(lineRow)
} }
lineAllocations lineAllocations
.slice(AllocationsTable.knowledge.sum(), AllocationsTable.risk.sum(), AllocationsTable.orphaned.sum()) .select(AllocationsTable.knowledge.sum(), AllocationsTable.risk.sum(), AllocationsTable.orphaned.sum())
.select { .where {
LinesTable.id eq lineId LinesTable.id eq lineId
}.first().let { }.first().let {
lineDict.stats = Statistics(it) lineDict.stats = Statistics(it)
@ -311,17 +311,18 @@ class SummaryModel(val db : Database) {
fileTree fileTree
} }
fun fileLines(fileId : Int) : List<String> = transaction(db) { fun fileLines(fileId: Int): List<String> = transaction(db) {
LinesTable.select { LinesTable.selectAll().where {
LinesTable.fileid eq fileId LinesTable.fileid eq fileId
}.orderBy(LinesTable.linenum).map { }.orderBy(LinesTable.linenum).map {
it[LinesTable.line] it[LinesTable.line]
} }
} }
private fun transformNode(tree : MutableMap<Int, ProjectTree>, dirId : Int) : ProjectTreeNode { private fun transformNode(tree: MutableMap<Int, ProjectTree>, dirId: Int): ProjectTreeNode {
val result = ProjectTreeNode() val result = ProjectTreeNode()
tree[dirId]?.let { dirdict -> tree[dirId]?.let { dirdict ->
result.name = dirdict.name
result.dirs = mutableListOf<ProjectTreeNode>().apply { result.dirs = mutableListOf<ProjectTreeNode>().apply {
dirdict.dirs.forEach { dirdict.dirs.forEach {
add(transformNode(tree, it)) add(transformNode(tree, it))
@ -333,11 +334,11 @@ class SummaryModel(val db : Database) {
return result return result
} }
private fun reconsDir(dirId : Int) = transaction(db) { private fun reconsDir(dirId: Int) = transaction(db) {
val segs = mutableListOf<String>() val segs = mutableListOf<String>()
var newDirId = dirId var newDirId = dirId
while(newDirId != 0) { while (newDirId != 0) {
DirsTable.select { DirsTable.selectAll().where {
DirsTable.id eq newDirId DirsTable.id eq newDirId
}.forEach { }.forEach {
segs.add(it[DirsTable.dir]) segs.add(it[DirsTable.dir])
@ -347,9 +348,9 @@ class SummaryModel(val db : Database) {
Paths.get(segs.reversed().joinToString("/")).normalize() 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 { AllocationsTable.insert {
it[AllocationsTable.knowledge] = knowledge it[AllocationsTable.knowledge] = knowledge
it[AllocationsTable.risk] = risk it[AllocationsTable.risk] = risk
@ -359,9 +360,9 @@ class SummaryModel(val db : Database) {
} }
} }
private fun findOrCreateAuthorGroup(authors : List<String>) : Int = transaction(db) { private fun findOrCreateAuthorGroup(authors: List<String>): Int = transaction(db) {
val authorsstr = authors.joinToString("\n") val authorsstr = authors.joinToString("\n")
var authorGroupId = AuthorsGroupsTable.select { var authorGroupId = AuthorsGroupsTable.selectAll().where {
AuthorsGroupsTable.authors eq authorsstr AuthorsGroupsTable.authors eq authorsstr
}.map { }.map {
it[AuthorsGroupsTable.id].value it[AuthorsGroupsTable.id].value
@ -382,18 +383,18 @@ class SummaryModel(val db : Database) {
authorGroupId authorGroupId
} }
private fun findOrCreateAuthor(author : String) : Int = transaction(db) { private fun findOrCreateAuthor(author: String): Int = transaction(db) {
AuthorsTable.insertIgnore { AuthorsTable.insertIgnore {
it[AuthorsTable.author] = author it[AuthorsTable.author] = author
} }
ProjectTable.select { AuthorsTable.selectAll().where {
AuthorsTable.author eq author AuthorsTable.author eq author
}.map { }.map {
it[AuthorsTable.id].value it[AuthorsTable.id].value
}.first() }.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 { LinesTable.insertAndGetId {
it[LinesTable.line] = line it[LinesTable.line] = line
it[LinesTable.linenum] = lineNum it[LinesTable.linenum] = lineNum
@ -401,31 +402,31 @@ class SummaryModel(val db : Database) {
}.value }.value
} }
private fun createFile(fname : String, parentDirId : Int) = transaction(db) { private fun createFile(fname: String, parentDirId: Int) = transaction(db) {
FilesTable.insertAndGetId { FilesTable.insertAndGetId {
it[FilesTable.fname] = fname it[FilesTable.fname] = fname
it[FilesTable.dirid] = parentDirId it[FilesTable.dirid] = parentDirId
}.value }.value
} }
private fun findOrCreateProject(project : String) : Int = transaction(db) { private fun findOrCreateProject(project: String): Int = transaction(db) {
ProjectTable.insertIgnore { ProjectTable.insertIgnore {
it[ProjectTable.project] = project it[ProjectTable.project] = project
} }
ProjectTable.select { ProjectTable.selectAll().where {
ProjectTable.project eq project ProjectTable.project eq project
}.map { }.map {
it[ProjectTable.id].value it[ProjectTable.id].value
}.first() }.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 { DirsTable.insertIgnore {
it[dir] = dirname it[dir] = dirname
it[parentdirid] = parentDirId it[parentdirid] = parentDirId
it[projectid] = projectId it[projectid] = projectId
} }
DirsTable.select { DirsTable.selectAll().where {
DirsTable.dir eq dirname DirsTable.dir eq dirname
DirsTable.parentdirid eq parentDirId DirsTable.parentdirid eq parentDirId
DirsTable.projectid eq projectId DirsTable.projectid eq projectId
@ -434,26 +435,26 @@ class SummaryModel(val db : Database) {
}.first() }.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 { private fun adjustFname(repoRoot: File, projectRoot: File, fname: File): File {
val rootDiff = if(projectRoot.canonicalPath != repoRoot.canonicalPath) { val rootDiff = if (projectRoot.canonicalPath != repoRoot.canonicalPath) {
projectRoot.relativeTo(repoRoot) projectRoot.relativeTo(repoRoot)
} else { } else {
repoRoot repoRoot
} }
return if(rootDiff.toString().length != 0) { return if (rootDiff.toString().length != 0) {
fname.relativeTo(rootDiff) fname.relativeTo(rootDiff)
} else { } else {
fname fname
} }
} }
private fun reconsDirs(dirId : Int) = transaction(db) { private fun reconsDirs(dirId: Int) = transaction(db) {
val dirs = mutableListOf<String>() val dirs = mutableListOf<String>()
var parentDirId = dirId var parentDirId = dirId
while(parentDirId != 0) { while (parentDirId != 0) {
DirsTable.select { DirsTable.selectAll().where {
DirsTable.id eq parentDirId DirsTable.id eq parentDirId
} }
.first() .first()
@ -476,4 +477,4 @@ class SummaryModel(val db : Database) {
AllocationsTable AllocationsTable
) )
} }
} }