├── projectreader └── build.gradle.kts ├── gradle.properties ├── .gitignore ├── settings.gradle.kts ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── screenshots ├── google_iosched_cloc_year.png ├── google_iosched_junit4_year.png └── google_iosched_lifecycle_year.png ├── gitHistoricalStats ├── src │ ├── main │ │ └── kotlin │ │ │ └── nl │ │ │ └── nielsvanhove │ │ │ └── githistoricalstats │ │ │ ├── charts │ │ │ ├── ChartBarData.kt │ │ │ ├── ChartRowData.kt │ │ │ ├── Chart.kt │ │ │ ├── ChartRenderer.kt │ │ │ └── ChartGenerator.kt │ │ │ ├── model │ │ │ ├── Granularity.kt │ │ │ └── AnnotatedCommit.kt │ │ │ ├── measurements │ │ │ ├── MeasurementConfig.kt │ │ │ ├── BashExecutor.kt │ │ │ ├── MeasurementRequirementValidator.kt │ │ │ ├── GrepExecutor.kt │ │ │ ├── ClocExecutor.kt │ │ │ └── MeasurementExecutor.kt │ │ │ ├── project │ │ │ ├── ProjectConfig.kt │ │ │ ├── ProjectDataReaderWriter.kt │ │ │ ├── ProjectData.kt │ │ │ └── ProjectConfigReader.kt │ │ │ ├── core │ │ │ ├── CommandExecutor.kt │ │ │ ├── GitWrapper.kt │ │ │ └── ImportantCommitFilter.kt │ │ │ └── Main.kt │ └── test │ │ └── kotlin │ │ └── nl │ │ └── nielsvanhove │ │ └── githistoricalstats │ │ ├── MeasurementExecutorTest.kt │ │ ├── ProjectDataTest.kt │ │ └── ImportantCommitFilterTest.kt └── build.gradle.kts ├── .github └── workflows │ └── gradle.yml ├── LICENSE ├── gradlew.bat ├── readme.md └── gradlew /projectreader/build.gradle.kts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .idea 3 | projects/* 4 | outputs/* 5 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "gitHistoricalStats" 2 | include(":gitHistoricalStats") 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nielsz/git-historical-stats/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /screenshots/google_iosched_cloc_year.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nielsz/git-historical-stats/HEAD/screenshots/google_iosched_cloc_year.png -------------------------------------------------------------------------------- /screenshots/google_iosched_junit4_year.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nielsz/git-historical-stats/HEAD/screenshots/google_iosched_junit4_year.png -------------------------------------------------------------------------------- /screenshots/google_iosched_lifecycle_year.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nielsz/git-historical-stats/HEAD/screenshots/google_iosched_lifecycle_year.png -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/charts/ChartBarData.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.charts 2 | 3 | data class ChartBarData(val offsets: List, val allData: List, val labels: List,) 4 | -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/model/Granularity.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.model 2 | 3 | enum class Granularity { 4 | YEAR, 5 | QUARTER, 6 | QUARTER12, 7 | MONTH, 8 | MONTH12 9 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/charts/ChartRowData.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.charts 2 | 3 | import java.time.OffsetDateTime 4 | 5 | data class ChartRowData(val date: OffsetDateTime, val data: Map) -------------------------------------------------------------------------------- /gitHistoricalStats/src/test/kotlin/nl/nielsvanhove/githistoricalstats/MeasurementExecutorTest.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats 2 | 3 | import org.junit.jupiter.api.Assertions.assertTrue 4 | import org.junit.jupiter.api.Test 5 | 6 | class MeasurementExecutorTest { 7 | 8 | @Test 9 | fun `When executing on an empty object`() { 10 | assertTrue(true) 11 | } 12 | } -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/model/AnnotatedCommit.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.model 2 | 3 | import java.time.OffsetDateTime 4 | 5 | data class AnnotatedCommit( 6 | val commitHash: String, 7 | val committerDate: OffsetDateTime, 8 | val lastOfYear: Boolean = false, 9 | val lastOfQuarter: Boolean = false, 10 | val lastOfMonth: Boolean = false, 11 | val isFirstCommit: Boolean = false, 12 | val isLastCommit: Boolean = false 13 | ) -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/measurements/MeasurementConfig.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.measurements 2 | 3 | sealed class MeasurementConfig(open val key: String) { 4 | data class BashMeasurementConfig(override val key: String, val command: String) : MeasurementConfig(key) 5 | data class GrepMeasurementConfig(override val key: String, val filetypes: List, val pattern: String) : MeasurementConfig(key) 6 | data class ClocMeasurementConfig(override val key: String, val filetypes: List, val folder: String) : MeasurementConfig(key) 7 | } 8 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | argParser = "2.0.7" 3 | serialization = "1.4.0" 4 | letsplotJvm = "4.0.0" 5 | letsplotExport = "2.4.0" 6 | junit = "5.9.0" 7 | 8 | [libraries] 9 | argParser = { module = "com.xenomachina:kotlin-argparser", version.ref = "argParser"} 10 | serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization"} 11 | letsplot-jvm = { module = "org.jetbrains.lets-plot:lets-plot-kotlin-jvm", version.ref = "letsplotJvm"} 12 | letsplot-export = { module = "org.jetbrains.lets-plot:lets-plot-image-export", version.ref = "letsplotExport"} 13 | junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit"} 14 | junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit"} 15 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Java CI with Gradle 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 11 20 | uses: actions/setup-java@v2 21 | with: 22 | java-version: '11' 23 | distribution: 'adopt' 24 | - name: Grant execute permission for gradlew 25 | run: chmod +x gradlew 26 | - name: Build with Gradle 27 | run: ./gradlew check build assembleDist 28 | -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/measurements/BashExecutor.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.measurements 2 | 3 | import nl.nielsvanhove.githistoricalstats.core.CommandExecutor 4 | import nl.nielsvanhove.githistoricalstats.measurements.MeasurementConfig.BashMeasurementConfig 5 | import nl.nielsvanhove.githistoricalstats.project.ProjectConfig 6 | 7 | class BashExecutor( 8 | private val projectConfig: ProjectConfig, 9 | private val commandExecutor: CommandExecutor, 10 | private val measurement: BashMeasurementConfig 11 | ) { 12 | operator fun invoke(): Int { 13 | val command = listOf("bash", "-c") + listOf(measurement.command) 14 | val output = commandExecutor.execute(command).trim() 15 | return output.toInt() 16 | } 17 | } -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/project/ProjectConfig.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.project 2 | 3 | import nl.nielsvanhove.githistoricalstats.charts.Chart 4 | import nl.nielsvanhove.githistoricalstats.measurements.MeasurementConfig 5 | import java.io.File 6 | import java.nio.file.Files 7 | 8 | data class ProjectConfig( 9 | val name: String, 10 | val repo: File, 11 | val branch: String, 12 | val filetypes: List, 13 | val measurements: List, 14 | val charts: List 15 | ) { 16 | fun validate() { 17 | if(repo.toPath().startsWith("~")) { 18 | throw RuntimeException("It's not possible to use relative paths. Use absolute path.") 19 | } 20 | 21 | if(!Files.exists(repo.toPath())) { 22 | throw RuntimeException("Git repository doesn't exist on the local file system: $repo") 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /gitHistoricalStats/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | kotlin("jvm") version "1.7.10" 5 | id("application") 6 | } 7 | 8 | group = "nl.nielsvanhove" 9 | version = "1.0.2" 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | implementation(libs.argParser) 17 | implementation(libs.serialization) 18 | implementation(libs.letsplot.jvm) 19 | implementation(libs.letsplot.export) 20 | 21 | testImplementation(kotlin("test-junit5")) 22 | testImplementation(libs.junit.api) 23 | testRuntimeOnly(libs.junit.engine) 24 | } 25 | 26 | tasks.test { 27 | useJUnitPlatform() 28 | testLogging { 29 | setEvents(listOf("PASSED", "SKIPPED", "FAILED", "STANDARD_OUT", "STANDARD_ERROR")) 30 | } 31 | } 32 | 33 | tasks.withType { 34 | kotlinOptions.jvmTarget = "11" 35 | } 36 | 37 | application { 38 | mainClass.set("nl.nielsvanhove.githistoricalstats.Main") 39 | } 40 | -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/core/CommandExecutor.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.core 2 | 3 | import nl.nielsvanhove.githistoricalstats.project.ProjectConfig 4 | import java.io.File 5 | import java.io.IOException 6 | 7 | class CommandExecutor(val projectConfig: ProjectConfig) { 8 | 9 | @Throws(IOException::class) 10 | fun execute(command: List, directory: File = projectConfig.repo, printError: Boolean = false): String { 11 | 12 | println("executing ${command.joinToString(" ")}") 13 | val process = ProcessBuilder(command) 14 | .directory(directory) 15 | .redirectOutput(ProcessBuilder.Redirect.PIPE) 16 | .redirectError(ProcessBuilder.Redirect.PIPE) 17 | .start() 18 | 19 | if (printError) { 20 | println("Error: " + process.errorStream.bufferedReader().readText()) 21 | } 22 | 23 | return process.inputStream.bufferedReader().readText() 24 | } 25 | } -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/measurements/MeasurementRequirementValidator.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.measurements 2 | 3 | import nl.nielsvanhove.githistoricalstats.core.CommandExecutor 4 | import nl.nielsvanhove.githistoricalstats.measurements.MeasurementConfig.ClocMeasurementConfig 5 | import nl.nielsvanhove.githistoricalstats.project.ProjectConfig 6 | 7 | class MeasurementRequirementValidator( 8 | private val projectConfig: ProjectConfig, 9 | private val commandExecutor: CommandExecutor 10 | ) { 11 | 12 | fun validate() { 13 | // If we're running cloc measurements, better make sure cloc is installed. 14 | val hasClocMeasurements = projectConfig.measurements.filterIsInstance().isNotEmpty() 15 | if (hasClocMeasurements && commandExecutor.execute(listOf("which", "cloc")).isEmpty()) { 16 | throw RuntimeException("cloc isn't installed. See https://github.com/AlDanial/cloc#install-via-package-manager") 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/measurements/GrepExecutor.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.measurements 2 | 3 | import nl.nielsvanhove.githistoricalstats.core.CommandExecutor 4 | import nl.nielsvanhove.githistoricalstats.measurements.MeasurementConfig.GrepMeasurementConfig 5 | import nl.nielsvanhove.githistoricalstats.project.ProjectConfig 6 | 7 | class GrepExecutor( 8 | private val projectConfig: ProjectConfig, 9 | private val commandExecutor: CommandExecutor, 10 | private val measurement: GrepMeasurementConfig 11 | ) { 12 | operator fun invoke(): Int { 13 | val fileTypeIncludes = measurement.filetypes 14 | .ifEmpty { projectConfig.filetypes } 15 | .joinToString(" ") { "--include=*.$it" } 16 | 17 | val c = "grep --recursive " + fileTypeIncludes + " -i -e '" + measurement.pattern + "' . | wc -l" 18 | val command = listOf("bash", "-c") + listOf(c) 19 | val output = commandExecutor.execute(command).trim() 20 | return output.toInt() 21 | } 22 | } -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/charts/Chart.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.charts 2 | 3 | /** 4 | * A complete chart 5 | */ 6 | data class Chart(val id: String, val items: List, val title: String, val subtitle: String? = null, val caption: String? = null, val legend: ChartLegend? = null) 7 | 8 | /** 9 | * One bar of a chart, which can contains multiple stacked items, but normally just one item. 10 | */ 11 | data class ChartStack(val items: List) 12 | 13 | data class ChartLegend(val title: String?, val items: List) 14 | 15 | 16 | val redChartColorsSmall = listOf("#D00000", "#E85D04", "#FAA307") 17 | val redChartColorsLarge = listOf("#1B4332", "#FF0A54", "#FFBA08","#FF6000","#74C69D", "#FF85A1","#B7E4C7","#FF9100","#D8F3DC","#FFAA00") 18 | val blueChartColorsSmall = listOf("#103E8F", "#16B9F5", "#C2E7FF") 19 | val blueChartColorsLarge = listOf("#103E8F", "#16B9F5", "#C2E7FF","#B95B13", "#944910", "#6E370C") 20 | 21 | val baseColors = listOf("#264653", "#2A9D8F", "#E9C46A", "#F4A261", "#E76F51","#6D6875", "#A5A58D") 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 nielsz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/measurements/ClocExecutor.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.measurements 2 | 3 | import kotlinx.serialization.json.Json 4 | import kotlinx.serialization.json.JsonObject 5 | import kotlinx.serialization.json.jsonPrimitive 6 | import nl.nielsvanhove.githistoricalstats.core.CommandExecutor 7 | import nl.nielsvanhove.githistoricalstats.measurements.MeasurementConfig.ClocMeasurementConfig 8 | import nl.nielsvanhove.githistoricalstats.project.ProjectConfig 9 | 10 | class ClocExecutor( 11 | private val projectConfig: ProjectConfig, 12 | private val commandExecutor: CommandExecutor, 13 | private val measurement: ClocMeasurementConfig 14 | ) { 15 | operator fun invoke(): Int { 16 | val fileTypeIncludes = if (measurement.filetypes.isNotEmpty()) { 17 | "--include-ext=" + measurement.filetypes.joinToString(",") 18 | } else { 19 | "" 20 | } 21 | 22 | val c = "cloc $fileTypeIncludes --json ${measurement.folder}" 23 | val command = listOf("bash", "-c") + listOf(c) 24 | val output = commandExecutor.execute(command).trim() 25 | 26 | if (output.isNotEmpty()) { 27 | val x = Json.parseToJsonElement(output) as JsonObject 28 | return (x["SUM"]!! as JsonObject)["code"]!!.jsonPrimitive.content.toInt() 29 | 30 | } 31 | return 0 32 | } 33 | } -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/core/GitWrapper.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.core 2 | 3 | import nl.nielsvanhove.githistoricalstats.project.ProjectConfig 4 | import java.time.OffsetDateTime 5 | 6 | class GitWrapper(private val projectConfig: ProjectConfig, private val commandExecutor: CommandExecutor) { 7 | 8 | fun reset() { 9 | val c = "git reset --hard HEAD" 10 | val command = listOf("bash", "-c") + listOf(c) 11 | commandExecutor.execute(command).trim() 12 | } 13 | 14 | fun log(): List { 15 | val c = "git log ${projectConfig.branch} --oneline --pretty=format:\"%h %cI %an\"" 16 | val command = listOf("bash", "-c") + listOf(c) 17 | val commits = commandExecutor.execute(command) 18 | .lines() 19 | .map { logLine -> 20 | val splittedData = logLine.split(" ") 21 | LogItem( 22 | commitHash = splittedData[0], 23 | committerDate = OffsetDateTime.parse(splittedData[1]), 24 | authorEmail = splittedData[2] 25 | ) 26 | } 27 | 28 | return commits 29 | } 30 | 31 | fun checkout(hash: String) { 32 | commandExecutor.execute(listOf("git", "checkout", hash)) 33 | commandExecutor.execute(listOf("git", "clean", "-fd")) 34 | 35 | } 36 | } 37 | 38 | data class LogItem(val commitHash: String, val committerDate: OffsetDateTime, val authorEmail: String) 39 | -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/project/ProjectDataReaderWriter.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.project 2 | 3 | import kotlinx.serialization.decodeFromString 4 | import kotlinx.serialization.encodeToString 5 | import kotlinx.serialization.json.* 6 | import java.io.File 7 | import java.io.FileNotFoundException 8 | 9 | object ProjectDataReaderWriter { 10 | 11 | fun read(projectName: String): ProjectData { 12 | val filename = "projects/$projectName.data.json" 13 | 14 | try { 15 | val content = File(filename).readText() 16 | if (content.isEmpty()) { 17 | return ProjectData(commits = JsonArray(content = listOf())) 18 | } 19 | val rootObject = Json.decodeFromString(content) 20 | return ProjectData(commits = rootObject) 21 | } catch(ex: FileNotFoundException) { 22 | return ProjectData(commits = JsonArray(content = listOf())) 23 | } 24 | } 25 | 26 | fun write(projectName: String, projectData: ProjectData) { 27 | val content = Json { 28 | prettyPrint = true 29 | }.encodeToString(projectData.commits) 30 | 31 | write(projectName, content) 32 | } 33 | 34 | fun write(projectName: String, specificCommit: JsonObject) { 35 | val hash = specificCommit["commitHash"]!!.jsonPrimitive.content 36 | val projectData = read(projectName) 37 | 38 | val updatedContent = (projectData.commits.filterNot { (it as JsonObject)["commitHash"]!!.jsonPrimitive.content == hash} + specificCommit).sortedBy { ((it as JsonObject)["committerDate"] as JsonPrimitive).content } 39 | 40 | val content = Json { 41 | prettyPrint = true 42 | }.encodeToString(updatedContent) 43 | 44 | write(projectName, content) 45 | } 46 | 47 | fun write(projectName: String, content: String) { 48 | val filename = "projects/$projectName.data.json" 49 | println("writing to $filename") 50 | File(filename).writeText(content) 51 | } 52 | } -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/charts/ChartRenderer.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.charts 2 | 3 | import org.jetbrains.letsPlot.Stat 4 | import org.jetbrains.letsPlot.geom.geomBar 5 | import org.jetbrains.letsPlot.intern.Plot 6 | import org.jetbrains.letsPlot.intern.Scale 7 | import org.jetbrains.letsPlot.label.labs 8 | import org.jetbrains.letsPlot.letsPlot 9 | import org.jetbrains.letsPlot.pos.positionStack 10 | import org.jetbrains.letsPlot.sampling.samplingNone 11 | import org.jetbrains.letsPlot.scale.scaleFillDiscrete 12 | import org.jetbrains.letsPlot.scale.scaleFillManual 13 | import org.jetbrains.letsPlot.scale.scaleXDiscrete 14 | 15 | class ChartRenderer(val chart: Chart, val data: List, val dates: List, val colors: List) { 16 | 17 | val totalBarWidth = 0.9 18 | val barWidth = totalBarWidth / data.size 19 | 20 | fun render(): Plot { 21 | 22 | var plot = letsPlot() + 23 | labs(x = "", y = "", title = chart.title, subtitle = chart.subtitle, caption = chart.caption) 24 | 25 | data.forEachIndexed { index, chartBarData -> 26 | plot += geomBar(stat = Stat.identity, position = positionStack, width = barWidth, sampling = samplingNone) { 27 | x = chartBarData.offsets.map { it + offsetForIndex(index, data.size) } 28 | y = chartBarData.allData 29 | fill = chartBarData.labels 30 | } 31 | } 32 | 33 | return plot + 34 | scaleFillManual(values = colors) + 35 | addLegend() + 36 | scaleXDiscrete(breaks = (1..dates.size).toList(), labels = dates) 37 | } 38 | 39 | private fun addLegend(): Scale { 40 | 41 | val legend = chart.legend 42 | val legendLabels = legend?.items ?: chart.items.flatMap { it.items } 43 | 44 | if (legendLabels.size <= 1) { 45 | // no legend if there's only one items. just write a good enough title. 46 | return scaleFillDiscrete(name = "") 47 | } 48 | 49 | val legendName = legend?.title ?: " " 50 | return scaleFillDiscrete(name = legendName, labels = legendLabels) 51 | } 52 | 53 | private fun offsetForIndex(index: Int, size: Int): Double { 54 | if (size == 1) return 0.0 55 | 56 | val shiftLeftABit = if (size % 2 == 0) (barWidth / 2) else barWidth 57 | return (index * barWidth) - shiftLeftABit 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/core/ImportantCommitFilter.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.core 2 | 3 | import nl.nielsvanhove.githistoricalstats.model.AnnotatedCommit 4 | import java.time.temporal.IsoFields 5 | 6 | /** 7 | * @param allCommits list of commits in reverse chronological order. 8 | */ 9 | class ImportantCommitFilter(val allCommits: List) { 10 | 11 | /** 12 | * Filters out all the commits that are not one of the following: 13 | * - The first commit 14 | * - The last commit 15 | * - The last commit of a year 16 | * - The last commit of a quarter 17 | * - The last commit of a month 18 | * 19 | */ 20 | fun filterImportantCommits(): List { 21 | val importantCommits = mutableListOf() 22 | 23 | allCommits.forEachIndexed { index, logItem -> 24 | val date = logItem.committerDate 25 | 26 | val thisYearDoestExistYet = importantCommits.none { importantCommit -> 27 | importantCommit.committerDate.year == date.year 28 | } 29 | 30 | val thisQuarterDoestExistYet = importantCommits.none { importantCommit -> 31 | importantCommit.committerDate.get(IsoFields.QUARTER_OF_YEAR) == date.get(IsoFields.QUARTER_OF_YEAR) && importantCommit.committerDate.year == date.year 32 | } 33 | 34 | val thisMonthDoestExistYet = importantCommits.none { importantCommit -> 35 | importantCommit.committerDate.monthValue == date.monthValue && importantCommit.committerDate.year == date.year 36 | } 37 | 38 | val isLast = index == 0 39 | val isFirst = index == allCommits.size - 1 40 | 41 | 42 | 43 | if (isFirst || isLast || thisYearDoestExistYet || thisQuarterDoestExistYet || thisMonthDoestExistYet) { 44 | importantCommits.add( 45 | AnnotatedCommit( 46 | logItem.commitHash, 47 | logItem.committerDate, 48 | lastOfMonth = !isLast && thisMonthDoestExistYet, 49 | lastOfQuarter = !isLast && thisQuarterDoestExistYet, 50 | lastOfYear = !isLast && thisYearDoestExistYet, 51 | isLastCommit = isLast, 52 | isFirstCommit = isFirst 53 | ) 54 | ) 55 | } 56 | } 57 | 58 | return importantCommits 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/project/ProjectData.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.project 2 | 3 | import kotlinx.serialization.json.JsonArray 4 | import kotlinx.serialization.json.JsonElement 5 | import kotlinx.serialization.json.JsonObject 6 | import kotlinx.serialization.json.JsonPrimitive 7 | import nl.nielsvanhove.githistoricalstats.model.AnnotatedCommit 8 | 9 | 10 | class ProjectData(var commits: JsonArray) { 11 | fun syncCommits(annotatedCommits: List) { 12 | 13 | val finalCommits = mutableListOf() 14 | for (commit in commits) { 15 | if (commit is JsonObject) { 16 | val commitHash = (commit["commitHash"] as JsonPrimitive).content 17 | val important = annotatedCommits.find { it.commitHash == commitHash } 18 | if (important != null) { 19 | 20 | val extraData = mapOf( 21 | "isFirstCommit" to JsonPrimitive(important.isFirstCommit), 22 | "isLastCommit" to JsonPrimitive(important.isLastCommit), 23 | "isLastOfYear" to JsonPrimitive(important.lastOfYear), 24 | "isLastOfQuarter" to JsonPrimitive(important.lastOfQuarter), 25 | "isLastOfMonth" to JsonPrimitive(important.lastOfMonth), 26 | ) 27 | 28 | finalCommits.add(JsonObject(commit + extraData)) 29 | } 30 | } 31 | } 32 | 33 | for (annotatedCommit in annotatedCommits) { 34 | val alreadyExists = 35 | commits.any { ((it as JsonObject)["commitHash"] as JsonPrimitive).content == annotatedCommit.commitHash } 36 | if (!alreadyExists) { 37 | finalCommits.add(newJsonObject(annotatedCommit)) 38 | } 39 | } 40 | 41 | finalCommits.sortBy { (it["committerDate"] as JsonPrimitive).content } 42 | 43 | commits = JsonArray(content = finalCommits) 44 | } 45 | 46 | fun newJsonObject(annotatedCommit: AnnotatedCommit): JsonObject { 47 | val map = mutableMapOf() 48 | map["commitHash"] = JsonPrimitive(annotatedCommit.commitHash) 49 | map["committerDate"] = JsonPrimitive(annotatedCommit.committerDate.toString()) 50 | map["isFirstCommit"] = JsonPrimitive(annotatedCommit.isFirstCommit) 51 | map["isLastCommit"] = JsonPrimitive(annotatedCommit.isLastCommit) 52 | map["isLastOfYear"] = JsonPrimitive(annotatedCommit.lastOfYear) 53 | map["isLastOfQuarter"] = JsonPrimitive(annotatedCommit.lastOfQuarter) 54 | map["isLastOfMonth"] = JsonPrimitive(annotatedCommit.lastOfMonth) 55 | map["measurements"] = JsonObject(emptyMap()) 56 | return JsonObject(content = map) 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/measurements/MeasurementExecutor.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.measurements 2 | 3 | import kotlinx.serialization.json.JsonElement 4 | import kotlinx.serialization.json.JsonObject 5 | import kotlinx.serialization.json.JsonPrimitive 6 | import kotlinx.serialization.json.jsonPrimitive 7 | import nl.nielsvanhove.githistoricalstats.core.CommandExecutor 8 | import nl.nielsvanhove.githistoricalstats.core.GitWrapper 9 | import nl.nielsvanhove.githistoricalstats.measurements.MeasurementConfig.* 10 | import nl.nielsvanhove.githistoricalstats.project.ProjectConfig 11 | 12 | class MeasurementExecutor( 13 | val projectConfig: ProjectConfig, 14 | val commandExecutor: CommandExecutor, 15 | val gitWrapper: GitWrapper 16 | ) { 17 | fun executeIfNeeded(commit: JsonObject, forceUpdateAll: Boolean, forceSpecificUpdate: String?): JsonObject? { 18 | 19 | val previousMeasurements = commit["measurements"] as JsonObject 20 | 21 | if (!shouldCheckout(previousMeasurements) && !forceUpdateAll && forceSpecificUpdate.isNullOrEmpty()) { 22 | return null 23 | } 24 | 25 | println("updating " + commit["committerDate"]) 26 | gitWrapper.checkout(commit["commitHash"]!!.jsonPrimitive.content) 27 | 28 | val measurementsToRun = mutableListOf() 29 | for (measurement in projectConfig.measurements) { 30 | if (!(previousMeasurements).containsKey(measurement.key) || forceUpdateAll || forceSpecificUpdate == measurement.key) { 31 | measurementsToRun.add(measurement) 32 | } 33 | } 34 | 35 | val result = executeMeasurements(measurementsToRun) 36 | val measurements = 37 | JsonObject(mapOf("measurements" to JsonObject((commit["measurements"] as JsonObject) + result))) 38 | return JsonObject(content = JsonObject(commit + measurements)) 39 | } 40 | 41 | private fun executeMeasurements(toRun: MutableList): JsonObject { 42 | val map = mutableMapOf() 43 | for (item in toRun) { 44 | map[item.key] = JsonPrimitive(executeMeasurement(item)) 45 | } 46 | 47 | return JsonObject(content = map) 48 | } 49 | 50 | 51 | private fun executeMeasurement(measurement: MeasurementConfig): Int { 52 | return when (measurement) { 53 | is BashMeasurementConfig -> { 54 | val executor = BashExecutor(projectConfig, commandExecutor, measurement) 55 | executor() 56 | } 57 | is GrepMeasurementConfig -> { 58 | val executor = GrepExecutor(projectConfig, commandExecutor, measurement) 59 | executor() 60 | } 61 | is ClocMeasurementConfig -> { 62 | val executor = ClocExecutor(projectConfig, commandExecutor, measurement) 63 | executor() 64 | } 65 | } 66 | } 67 | 68 | 69 | private fun shouldCheckout(previousMeasurements: JsonObject): Boolean { 70 | for (measurement in projectConfig.measurements) { 71 | if (!previousMeasurements.containsKey(measurement.key)) { 72 | return true 73 | } 74 | } 75 | 76 | return false 77 | } 78 | } -------------------------------------------------------------------------------- /gitHistoricalStats/src/test/kotlin/nl/nielsvanhove/githistoricalstats/ProjectDataTest.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats 2 | 3 | import kotlinx.serialization.json.* 4 | import nl.nielsvanhove.githistoricalstats.model.AnnotatedCommit 5 | import nl.nielsvanhove.githistoricalstats.project.ProjectData 6 | import org.junit.jupiter.api.Assertions.assertEquals 7 | import org.junit.jupiter.api.Test 8 | import java.time.OffsetDateTime 9 | 10 | class ProjectDataTest { 11 | 12 | @Test 13 | fun `Given no initial commits and no new commits, When Syncing, Then the list will be empty`() { 14 | 15 | // Given 16 | val commits = JsonArray(emptyList()) 17 | val projectData = ProjectData(commits = commits) 18 | 19 | // When 20 | projectData.syncCommits(annotatedCommits = listOf()) 21 | 22 | // Then 23 | assertEquals(emptyList(), projectData.commits) 24 | } 25 | 26 | @Test 27 | fun `Given two initial commits and no new commits, When Syncing, Then the list will be empty`() { 28 | 29 | // Given 30 | val commits = JsonArray(listOf(projectDataCommit("a"), projectDataCommit("b"))) 31 | val projectData = ProjectData(commits = commits) 32 | 33 | // When 34 | projectData.syncCommits(annotatedCommits = listOf()) 35 | 36 | // Then 37 | assertEquals(emptyList(), projectData.commits) 38 | } 39 | 40 | @Test 41 | fun `Given no initial commits and two new commits, When Syncing, Then the list will the two new items`() { 42 | 43 | // Given 44 | val commits = JsonArray(emptyList()) 45 | val projectData = ProjectData(commits = commits) 46 | 47 | // When 48 | projectData.syncCommits(annotatedCommits = listOf(annotatedCommit("a"), annotatedCommit("b"))) 49 | 50 | // Then 51 | assertEquals(2, projectData.commits.size) 52 | assertEquals("a", (projectData.commits[0] as JsonObject)["commitHash"]!!.jsonPrimitive.content) 53 | assertEquals("b", (projectData.commits[1] as JsonObject)["commitHash"]!!.jsonPrimitive.content) 54 | } 55 | 56 | @Test 57 | fun `Given there is one initial commits with custom data, When Syncing, Then the list will retain that custom data`() { 58 | val content = mutableMapOf() 59 | content["commitHash"] = JsonPrimitive("q") 60 | content["committerDate"] = JsonPrimitive(OffsetDateTime.now().toString()) 61 | content["otherStuff"] = JsonArray(content = listOf(JsonPrimitive("p"), JsonPrimitive(18))) 62 | val jsonObject = JsonObject(content = content) 63 | 64 | // Given 65 | val commits = JsonArray(listOf(jsonObject)) 66 | val projectData = ProjectData(commits = commits) 67 | 68 | // When 69 | projectData.syncCommits(annotatedCommits = listOf(annotatedCommit("a"), annotatedCommit("q"))) 70 | 71 | println(projectData.commits) 72 | // Then 73 | assertEquals(2, projectData.commits.size) 74 | val q = (projectData.commits[0] as JsonObject) 75 | assertEquals("q", q["commitHash"]!!.jsonPrimitive.content) 76 | assertEquals("p", q["otherStuff"]!!.jsonArray[0].jsonPrimitive.content) 77 | assertEquals("18", q["otherStuff"]!!.jsonArray[1].jsonPrimitive.content) 78 | assertEquals("a", (projectData.commits[1] as JsonObject)["commitHash"]!!.jsonPrimitive.content) 79 | 80 | 81 | } 82 | 83 | private fun annotatedCommit(hash: String): AnnotatedCommit { 84 | return AnnotatedCommit( 85 | commitHash = hash, 86 | committerDate = OffsetDateTime.now(), 87 | lastOfYear = false, 88 | lastOfQuarter = false, 89 | lastOfMonth = false, 90 | isFirstCommit = false, 91 | isLastCommit = false 92 | ) 93 | } 94 | 95 | private fun projectDataCommit(hash: String): JsonObject { 96 | val content = mutableMapOf() 97 | content["commitHash"] = JsonPrimitive(hash) 98 | return JsonObject(content = content) 99 | } 100 | } -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/Main.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats 2 | 3 | import com.xenomachina.argparser.ArgParser 4 | import com.xenomachina.argparser.default 5 | import kotlinx.serialization.json.JsonObject 6 | import nl.nielsvanhove.githistoricalstats.charts.ChartGenerator 7 | import nl.nielsvanhove.githistoricalstats.core.CommandExecutor 8 | import nl.nielsvanhove.githistoricalstats.core.GitWrapper 9 | import nl.nielsvanhove.githistoricalstats.core.ImportantCommitFilter 10 | import nl.nielsvanhove.githistoricalstats.measurements.MeasurementExecutor 11 | import nl.nielsvanhove.githistoricalstats.project.ProjectConfig 12 | import nl.nielsvanhove.githistoricalstats.project.ProjectConfigReader 13 | import nl.nielsvanhove.githistoricalstats.project.ProjectDataReaderWriter 14 | import java.io.File 15 | import nl.nielsvanhove.githistoricalstats.measurements.MeasurementRequirementValidator 16 | 17 | class Main { 18 | companion object { 19 | @JvmStatic 20 | fun main(args: Array) { 21 | createFoldersIfNeeded() 22 | 23 | val arguments = ArgParser(args).parseInto(::ApplicationArgs) 24 | val projectConfig = ProjectConfigReader.read(arguments.project) 25 | projectConfig.validate() 26 | 27 | val commandExecutor = CommandExecutor(projectConfig) 28 | 29 | MeasurementRequirementValidator(projectConfig, commandExecutor).validate() 30 | 31 | val gitWrapper = GitWrapper(projectConfig, commandExecutor) 32 | gitWrapper.reset() 33 | 34 | syncCommits(projectConfig, gitWrapper) 35 | executeMeasurements(projectConfig, commandExecutor, gitWrapper, arguments) 36 | generateCharts(projectConfig) 37 | 38 | gitWrapper.reset() 39 | } 40 | } 41 | } 42 | 43 | private fun generateCharts(projectConfig: ProjectConfig) { 44 | val projectData = ProjectDataReaderWriter.read(projectConfig.name) 45 | 46 | val chartGenerator = ChartGenerator(projectConfig, projectData) 47 | for (chart in projectConfig.charts) { 48 | println("generating $chart") 49 | chartGenerator.generate(chart) 50 | } 51 | } 52 | 53 | private fun executeMeasurements( 54 | projectConfig: ProjectConfig, 55 | commandExecutor: CommandExecutor, 56 | gitWrapper: GitWrapper, 57 | args: ApplicationArgs 58 | ) { 59 | val projectData = ProjectDataReaderWriter.read(projectConfig.name) 60 | val measurementExecutor = MeasurementExecutor(projectConfig, commandExecutor, gitWrapper) 61 | for (commit in projectData.commits) { 62 | 63 | 64 | val updatedCommit = 65 | measurementExecutor.executeIfNeeded( 66 | commit as JsonObject, 67 | forceUpdateAll = args.runAllMeasurements, 68 | forceSpecificUpdate = args.rerunMeasurement 69 | ) 70 | if (updatedCommit != null) { 71 | ProjectDataReaderWriter.write(projectConfig.name, updatedCommit) 72 | } 73 | } 74 | } 75 | 76 | private fun syncCommits(projectConfig: ProjectConfig, gitWrapper: GitWrapper) { 77 | 78 | val commits = gitWrapper.log() 79 | val importantCommitFilter = ImportantCommitFilter(commits) 80 | val annotatedCommits = importantCommitFilter.filterImportantCommits() 81 | 82 | val projectData = ProjectDataReaderWriter.read(projectConfig.name) 83 | projectData.syncCommits(annotatedCommits) 84 | ProjectDataReaderWriter.write(projectConfig.name, projectData) 85 | } 86 | 87 | private fun createFoldersIfNeeded() { 88 | val folders = listOf("projects", "repos", "output") 89 | folders.filter { !File(it).exists() }.forEach { File(it).mkdir() } 90 | } 91 | 92 | class ApplicationArgs(parser: ArgParser) { 93 | 94 | val project by parser.storing("-p", "--project", help = "project name") 95 | 96 | val rerunMeasurement by parser.storing("--rerunMeasurement", help = "Rerun one measurement") 97 | .default(null) 98 | 99 | val runAllMeasurements by parser.flagging("--runAllMeasurements", help = "(Re)run all measurements") 100 | .default(defaultValue = false) 101 | } 102 | -------------------------------------------------------------------------------- /gitHistoricalStats/src/test/kotlin/nl/nielsvanhove/githistoricalstats/ImportantCommitFilterTest.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats 2 | 3 | import nl.nielsvanhove.githistoricalstats.core.ImportantCommitFilter 4 | import nl.nielsvanhove.githistoricalstats.core.LogItem 5 | import nl.nielsvanhove.githistoricalstats.model.AnnotatedCommit 6 | import org.junit.jupiter.api.Test 7 | import org.junit.jupiter.api.Assertions.* 8 | import java.time.OffsetDateTime 9 | import java.time.ZoneOffset 10 | 11 | class ImportantCommitFilterTest { 12 | 13 | @Test 14 | fun `When filtering an empty list, Then return an empty list`() { 15 | // When 16 | val result = ImportantCommitFilter(emptyList()).filterImportantCommits() 17 | 18 | // Then 19 | assertEquals(emptyList(), result) 20 | } 21 | 22 | @Test 23 | fun `When filtering one item, Then it is both the first and the last`() { 24 | // Given 25 | val data = listOf( 26 | logItem("a", date(2021, 7, 1)) 27 | ) 28 | 29 | // When 30 | val result = ImportantCommitFilter(data).filterImportantCommits() 31 | 32 | // Then 33 | assertEquals(1, result.size) 34 | assertEquals("a", result[0].commitHash) 35 | assertTrue(result[0].isLastCommit) 36 | assertTrue(result[0].isFirstCommit) 37 | } 38 | 39 | @Test 40 | fun `When the first commit was in december, Then the first item is the last of the year`() { 41 | // Given 42 | val data = listOf( 43 | logItem("a", date(2022, 1, 1)), 44 | logItem("b", date(2021, 12, 31)) 45 | ) 46 | 47 | // When 48 | val result = ImportantCommitFilter(data).filterImportantCommits() 49 | 50 | // Then 51 | assertEquals(2, result.size) 52 | assertEquals("a", result[0].commitHash) 53 | assertTrue(result[0].isLastCommit) 54 | assertFalse(result[0].isFirstCommit) 55 | assertFalse(result[0].lastOfYear) 56 | assertFalse(result[0].lastOfQuarter) 57 | assertFalse(result[0].lastOfMonth) 58 | 59 | assertEquals("b", result[1].commitHash) 60 | assertFalse(result[1].isLastCommit) 61 | assertTrue(result[1].isFirstCommit) 62 | assertTrue(result[1].lastOfYear) 63 | assertTrue(result[1].lastOfQuarter) 64 | assertTrue(result[1].lastOfMonth) 65 | } 66 | 67 | 68 | @Test 69 | fun `When filtering one item, Then it is never the last of the year`() { 70 | // Given 71 | val data = listOf( 72 | logItem("a", date(2021, 12, 31)) 73 | ) 74 | 75 | // When 76 | val result = ImportantCommitFilter(data).filterImportantCommits() 77 | 78 | // Then 79 | assertEquals(1, result.size) 80 | assertEquals("a", result[0].commitHash) 81 | assertTrue(result[0].isLastCommit) 82 | assertFalse(result[0].lastOfYear) 83 | assertFalse(result[0].lastOfQuarter) 84 | assertFalse(result[0].lastOfMonth) 85 | } 86 | 87 | @Test 88 | fun `When filtering three items from the same month, Then only the latest is added`() { 89 | // Given 90 | val data = listOf( 91 | logItem("a", date(2021, 7, 3)), 92 | logItem("b", date(2021, 7, 2)), 93 | logItem("c", date(2021, 7, 1)) 94 | ) 95 | 96 | // When 97 | val result = ImportantCommitFilter(data).filterImportantCommits() 98 | 99 | // Then 100 | assertEquals(2, result.size) 101 | assertEquals("a", result[0].commitHash) 102 | assertTrue(result[0].isLastCommit) 103 | assertEquals("c", result[1].commitHash) 104 | assertTrue(result[1].isFirstCommit) 105 | } 106 | 107 | @Test 108 | fun `When filtering four items from two months, Then three items are added`() { 109 | // Given 110 | val data = listOf( 111 | logItem("a", date(2021, 7, 3)), 112 | logItem("b", date(2021, 7, 2)), 113 | logItem("c", date(2021, 6, 30)), 114 | logItem("d", date(2021, 6, 16)) 115 | ) 116 | 117 | // When 118 | val result = ImportantCommitFilter(data).filterImportantCommits() 119 | 120 | // Then 121 | assertEquals(3, result.size) 122 | assertEquals("a", result[0].commitHash) 123 | assertTrue(result[0].isLastCommit) 124 | assertFalse(result[0].lastOfYear) 125 | assertFalse(result[0].lastOfQuarter) 126 | assertFalse(result[0].lastOfMonth) 127 | assertEquals("c", result[1].commitHash) 128 | assertTrue(result[1].lastOfQuarter) 129 | assertFalse(result[1].lastOfYear) 130 | assertTrue(result[1].lastOfQuarter) 131 | assertTrue(result[1].lastOfMonth) 132 | assertEquals("d", result[2].commitHash) 133 | assertTrue(result[2].isFirstCommit) 134 | assertFalse(result[2].lastOfYear) 135 | assertFalse(result[2].lastOfQuarter) 136 | assertFalse(result[2].lastOfMonth) 137 | } 138 | 139 | @Test 140 | fun `When filtering five items from two months from different years, Then three items are added`() { 141 | // Given 142 | val data = listOf( 143 | logItem("a", date(2021, 7, 3)), 144 | logItem("b", date(2021, 6, 30)), 145 | logItem("c", date(2020, 6, 18)), 146 | logItem("d", date(2020, 1, 1)) 147 | ) 148 | 149 | // When 150 | val result = ImportantCommitFilter(data).filterImportantCommits() 151 | 152 | // Then 153 | assertEquals(4, result.size) 154 | assertEquals("a", result[0].commitHash) 155 | assertTrue(result[0].isLastCommit) 156 | assertEquals("b", result[1].commitHash) 157 | assertEquals("c", result[2].commitHash) 158 | assertEquals("d", result[3].commitHash) 159 | assertTrue(result[3].isFirstCommit) 160 | } 161 | 162 | 163 | fun date(year: Int, month: Int, day: Int) = OffsetDateTime.of(year, month, day, 0, 0, 0, 0, ZoneOffset.UTC) 164 | 165 | fun logItem( 166 | commitHash: String = List(8) { 167 | (('a'..'z') + ('A'..'Z') + ('0'..'9')).random() 168 | }.joinToString(""), 169 | committerDate: OffsetDateTime 170 | ) = LogItem( 171 | commitHash, committerDate = committerDate, authorEmail = "niels.vanhove@gmail.com" 172 | ) 173 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Git Historical Stats 2 | 3 | An iterative commandline tool to generate statistics on long-term projects. This tools consists of three steps: 4 | 5 | 1. Get "important" commits. 6 | 2. Calculate measurements for those commits. 7 | 3. Generate charts on those measurements. 8 | 9 | These charts can be useful when you're doing long-term migrations which can't be done overnight, but you want to keep progress. 10 | 11 | It's iterative, so the second time this script runs, it will only calculate the latest commits and new measurements and 12 | charts. 13 | 14 | --- 15 | 16 | As an example, there are three samples from Google IO's Schedule app (https://github.com/google/iosched), which is 17 | updated each year with the latest technologies. One odd thing in this repo is that it's not an entire-year process, but gets only worked on for a few months. Therefore you can see that the last commit of 2018 was in August. 18 | 19 | 1. Lines of code per year. 20 | You can see that in 2018 the app was rewritten in Kotlin and that a lot less code was needed. 21 | ![Google IOSched Lines of code per year](https://github.com/nielsz/project-info/blob/main/screenshots/google_iosched_cloc_year.png?raw=true) 22 | 23 | 24 | 2. Number of files that contain `import org.junit.Test` and number of `@Test` annotations. ![Google IOSched Junit4](https://github.com/nielsz/project-info/blob/main/screenshots/google_iosched_junit4_year.png?raw=true) 25 | 26 | 27 | 3. Activities and Fragments. Google has moved towards Fragments without Activities for the last few years now. ![Google IOSched lifecycle](https://github.com/nielsz/project-info/blob/main/screenshots/google_iosched_lifecycle_year.png?raw=true) 28 | 29 | ## Installation 30 | 1. Download the latest release, and place it somewhere. `~/gitHistoricalStats` would be nice. 31 | 2. Run `./gitHistoricalStats` within the `bin` directory. It will create directories `repos`,`projects`, and `output`. 32 | 3. Do a new clone of your project to the `repos` directory. This is because this script will checkout all the important commits, and does some `git reset --hard HEAD` at the beginning and the end. You want to avoid running this script while there are uncommitted changes. 33 | 4. Add a `projects/myproject.config.json` with the following structure: 34 | ``` 35 | { 36 | "repo": "/Users/username/gitHistoricalStats/repos/myproject", 37 | "branch": "develop", 38 | "filetypes":["kt","java"], 39 | "charts":[], 40 | "measurements":[] 41 | } 42 | ``` 43 | ## Usage 44 | Run `./gitHistoricalStats --project=myproject` from the `bin` directory. It will run all the measurements and charts.
45 | Run `./gitHistoricalStats --project=myproject --runAllMeasurements` to rerun all the measurements, even if they were already done.
46 | Run `./gitHistoricalStats --project=myproject --rerunMeasurement=junit4imports` to rerun the measurement `junit4imports`, even if this was already done.
47 | 48 | If there are measurements and charts defined, the charts will be stored in the `output` directory. 49 | 50 | --- 51 | ## Step 1 52 | 53 | Based on the config file which contains the path to the local repo and the branch, it will run a git log, and will store 54 | the important commits in the data file. A commit will be considered important if it's the first commit, the last commit, 55 | or the last commit of a month, quarter or year. 56 | 57 | ## Step 2 58 | 59 | For each commit, it will run measurements. A measurement can be cloc, a simple grep call, or a custom written bash 60 | script. 61 | 62 | #### Type: CLOC 63 | 64 | This calculates the sum of all files that have the extension `.kt` 65 | 66 | ``` 67 | { 68 | "type": "cloc", 69 | "key": "cloc_kotlin", 70 | "filetypes": [ 71 | "kt" 72 | ] 73 | } 74 | ``` 75 | 76 | #### Type: GREP 77 | 78 | This calculates the amount of times a certain grep is found in the codebase. 79 | 80 | ``` 81 | { 82 | "type": "grep", 83 | "key": "junit4imports", 84 | "pattern": "import org.junit.Test" 85 | } 86 | ``` 87 | 88 | #### Type: BASH 89 | 90 | This allows you to run your own custom scripts to calculate something. It should always output a number. The following 91 | example calculates the amount of Android Activities. 92 | 93 | ``` 94 | { 95 | "type": "bash", 96 | "key": "activities", 97 | "command": "git ls-files | grep \"AndroidManifest.xml\" | xargs cat | grep \", granularity: Granularity): List { 24 | val returnValues = mutableListOf() 25 | for (commit in projectData.commits) { 26 | val commitMeasurementValues = mutableMapOf() 27 | val measurements = (commit as JsonObject)["measurements"] as JsonObject 28 | for (s in list) { 29 | if(!measurements.containsKey(s)) throw IllegalArgumentException("Measurement `$s` does not exist.") 30 | commitMeasurementValues[s] = (measurements[s]!!.jsonPrimitive.content).toInt() 31 | } 32 | 33 | if (commitMeasurementValues.values.sum() > 0 || returnValues.size > 0) { 34 | val isLastOfYear = commit["isLastOfYear"]!!.jsonPrimitive.toString().toBoolean() 35 | val isLastOfQuarter = commit["isLastOfQuarter"]!!.jsonPrimitive.toString().toBoolean() 36 | val isLastOfMonth = commit["isLastOfMonth"]!!.jsonPrimitive.toString().toBoolean() 37 | val isFirstCommit = commit["isFirstCommit"]!!.jsonPrimitive.toString().toBoolean() 38 | val isLastCommit = commit["isLastCommit"]!!.jsonPrimitive.toString().toBoolean() 39 | 40 | if (isFirstCommit || isLastCommit || (granularity == YEAR && isLastOfYear) || (granularity == QUARTER && isLastOfQuarter) || (granularity == QUARTER12 && isLastOfQuarter) || (granularity == MONTH && isLastOfMonth) || (granularity == MONTH12 && isLastOfMonth)) { 41 | val date = OffsetDateTime.parse(commit["committerDate"]!!.jsonPrimitive.content) 42 | returnValues.add(ChartRowData(date = date, data = commitMeasurementValues)) 43 | } 44 | } 45 | } 46 | 47 | if (granularity == MONTH12 || granularity == QUARTER12) { 48 | return returnValues.takeLast(12).toMutableList() 49 | } 50 | 51 | return returnValues 52 | } 53 | 54 | fun generate(chart: Chart) { 55 | generate(chart, YEAR, 1) 56 | generate(chart, QUARTER, 2) 57 | generate(chart, QUARTER12, 3) 58 | generate(chart, MONTH, 4) 59 | generate(chart, MONTH12, 5) 60 | } 61 | 62 | 63 | private fun generate(chart: Chart, granularity: Granularity, offset: Int) { 64 | val id = chart.id + "_" + offset + "_" + granularity.toString().lowercase(Locale.getDefault()) 65 | 66 | val content = getContent(chart.items.flatMap { it.items }, granularity) 67 | val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") 68 | val dates = content.map { formatter.format(it.date) } 69 | 70 | val data = mutableListOf() 71 | 72 | for (itemX in chart.items) { 73 | val allData = mutableListOf() 74 | for (chartRowData in content) { 75 | for (key in itemX.items) { 76 | allData.add(chartRowData.data[key]!!) 77 | } 78 | } 79 | 80 | data.add( 81 | ChartBarData( 82 | offsets = generateOffsets(itemX.items.size, allData.size), 83 | allData = allData, 84 | labels = repeatlabels(itemX.items, allData.size) 85 | ) 86 | ) 87 | } 88 | 89 | val renderer = ChartRenderer(chart = chart, data = data, dates = dates, colors = getColorsFor(chart.items)) 90 | val plot = renderer.render() 91 | ggsave(plot, "$id.png", path = "output/" + projectConfig.name) 92 | } 93 | 94 | private fun getColorsFor(items: List): List { 95 | val colors = mutableListOf() 96 | 97 | val totalColors = items.flatMap { it.items }.size 98 | if (totalColors <= baseColors.size) { 99 | colors.addAll(baseColors.subList(0, totalColors)) 100 | } else { 101 | items.forEachIndexed { index, chartStack -> 102 | val source = if (index == 0) { 103 | if (chartStack.items.size <= 3) { 104 | redChartColorsSmall 105 | } else { 106 | redChartColorsLarge 107 | } 108 | } else { 109 | if (chartStack.items.size <= 3) { 110 | blueChartColorsSmall 111 | } else { 112 | blueChartColorsLarge 113 | } 114 | } 115 | colors.addAll(source.subList(0, chartStack.items.size)) 116 | } 117 | } 118 | 119 | 120 | 121 | return colors 122 | } 123 | 124 | private fun generateOffsets(itemCount: Int, dataSize: Int): List { 125 | val list = mutableListOf() 126 | 127 | var i = 0 128 | while (list.size < dataSize) { 129 | i++ 130 | for (j in (1..itemCount)) { 131 | list.add(i) 132 | } 133 | } 134 | 135 | return list 136 | } 137 | 138 | private fun repeatlabels(initialList: List, upTo: Int): List { 139 | val result = generateSequence { initialList } 140 | .flatten() 141 | .take(upTo) 142 | .toList() 143 | 144 | return result 145 | } 146 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gitHistoricalStats/src/main/kotlin/nl/nielsvanhove/githistoricalstats/project/ProjectConfigReader.kt: -------------------------------------------------------------------------------- 1 | package nl.nielsvanhove.githistoricalstats.project 2 | 3 | import java.io.File 4 | import java.io.FileNotFoundException 5 | import kotlinx.serialization.decodeFromString 6 | import kotlinx.serialization.json.Json 7 | import kotlinx.serialization.json.JsonArray 8 | import kotlinx.serialization.json.JsonObject 9 | import kotlinx.serialization.json.JsonPrimitive 10 | import kotlinx.serialization.json.jsonArray 11 | import kotlinx.serialization.json.jsonObject 12 | import kotlinx.serialization.json.jsonPrimitive 13 | import nl.nielsvanhove.githistoricalstats.charts.Chart 14 | import nl.nielsvanhove.githistoricalstats.charts.ChartLegend 15 | import nl.nielsvanhove.githistoricalstats.charts.ChartStack 16 | import nl.nielsvanhove.githistoricalstats.measurements.MeasurementConfig 17 | import nl.nielsvanhove.githistoricalstats.measurements.MeasurementConfig.BashMeasurementConfig 18 | import nl.nielsvanhove.githistoricalstats.measurements.MeasurementConfig.ClocMeasurementConfig 19 | import nl.nielsvanhove.githistoricalstats.measurements.MeasurementConfig.GrepMeasurementConfig 20 | 21 | object ProjectConfigReader { 22 | 23 | fun read(projectName: String): ProjectConfig { 24 | val filename = "projects/$projectName.config.json" 25 | val rootObject = try { 26 | Json.decodeFromString(File(filename).readText()) 27 | } catch (ex: FileNotFoundException) { 28 | val absolutePath = File("").absolutePath 29 | System.err.println("Can't find project. Create $absolutePath/$filename in the projects folder.") 30 | System.err.println( 31 | "{\n" + 32 | " \"repo\": \"My local path to the repository\",\n" + 33 | " \"branch\": \"develop\",\n" + 34 | " \"filetypes\":[\"kt\",\"java\"],\n" + 35 | " \"charts\":[],\n" + 36 | " \"measurements\":[]\n" + 37 | "}" 38 | ) 39 | throw ex 40 | } 41 | 42 | val repo = rootObject["repo"]!!.jsonPrimitive.content 43 | val branch = rootObject["branch"]!!.jsonPrimitive.content 44 | 45 | val measurementsJson = rootObject["measurements"] as JsonArray? 46 | val patterns = mutableListOf() 47 | if (measurementsJson != null) { 48 | for (measurementJson in measurementsJson) { 49 | assert((measurementJson as JsonObject)["type"] != null, "Missing `type` string field in measurement.") 50 | assert(measurementJson["key"] != null, "Missing `key` string field in measurement.") 51 | val type = measurementJson["type"]!!.jsonPrimitive.content 52 | when (type) { 53 | "bash" -> { 54 | assert(measurementJson["command"] != null, "Bash measurement needs a `command` string field.") 55 | patterns.add( 56 | BashMeasurementConfig( 57 | key = measurementJson["key"]!!.jsonPrimitive.content, 58 | command = measurementJson["command"]!!.jsonPrimitive.content, 59 | ) 60 | ) 61 | } 62 | "grep" -> { 63 | assert(measurementJson["pattern"] != null, "Grep measurement needs a `pattern` string field.") 64 | val types = if (measurementJson.containsKey("filetypes")) { 65 | measurementJson["filetypes"]!!.jsonArray.map { it.jsonPrimitive.content } 66 | } else { 67 | emptyList() 68 | } 69 | 70 | patterns.add( 71 | GrepMeasurementConfig( 72 | key = measurementJson["key"]!!.jsonPrimitive.content, 73 | filetypes = types, 74 | pattern = measurementJson["pattern"]!!.jsonPrimitive.content, 75 | ) 76 | ) 77 | } 78 | "cloc" -> { 79 | assert( 80 | measurementJson["filetypes"] != null, 81 | "Cloc measurement needs a `filetypes` array field." 82 | ) 83 | val filetypes = measurementJson["filetypes"]!!.jsonArray.map { it.jsonPrimitive.content } 84 | val folder = if (measurementJson.containsKey("folder")) { 85 | measurementJson["folder"]!!.jsonPrimitive.content 86 | } else "." 87 | 88 | patterns.add( 89 | ClocMeasurementConfig( 90 | key = measurementJson["key"]!!.jsonPrimitive.content, 91 | filetypes = filetypes, 92 | folder = folder 93 | ) 94 | ) 95 | } 96 | } 97 | } 98 | } 99 | 100 | val filetypes = mutableListOf() 101 | for (jsonElement in rootObject["filetypes"]!!.jsonArray) { 102 | filetypes.add(jsonElement.jsonPrimitive.content) 103 | } 104 | 105 | val charts = mutableListOf() 106 | val chartsArray = rootObject["charts"]?.jsonArray ?: emptyList() 107 | for (jsonElement in chartsArray) { 108 | val item = jsonElement.jsonObject 109 | assert(item["items"] != null, "An `items` array is needed to plot a chart.") 110 | assert(item["items"] is JsonArray, "The `items` element needs to be an array. One item represents one bar.") 111 | 112 | val chartStacks = mutableListOf() 113 | for (jsonBarElement in item["items"]!!.jsonArray) { 114 | assert( 115 | jsonBarElement is JsonArray, 116 | "The `items` sub-element needs to be an array. One item represents one item in the stacked bar." 117 | ) 118 | chartStacks.add(ChartStack((jsonBarElement as JsonArray).map { it.jsonPrimitive.content })) 119 | } 120 | 121 | val legend = item["legend"] as JsonObject? 122 | val chartLegend = if (legend != null) { 123 | val legendTitle = if (legend.containsKey("title")) { 124 | (legend["title"] as JsonPrimitive).content 125 | } else { 126 | null 127 | } 128 | val legendItems = (legend["items"] as JsonArray).map { it.jsonPrimitive.content } 129 | ChartLegend(title = legendTitle, items = legendItems) 130 | } else { 131 | null 132 | } 133 | 134 | val chart = Chart( 135 | id = item["id"]!!.jsonPrimitive.content, 136 | items = chartStacks, 137 | legend = chartLegend, 138 | title = item["title"]!!.jsonPrimitive.content, 139 | subtitle = item["subtitle"]?.jsonPrimitive?.content, 140 | caption = item["caption"]?.jsonPrimitive?.content, 141 | ) 142 | charts.add(chart) 143 | } 144 | 145 | return ProjectConfig( 146 | name = projectName, 147 | repo = File(repo), 148 | branch = branch, 149 | filetypes, 150 | measurements = patterns, 151 | charts = charts 152 | ) 153 | } 154 | 155 | private fun assert(assertion: Boolean, error: String) { 156 | if (!assertion) throw IllegalArgumentException(error) 157 | } 158 | } 159 | --------------------------------------------------------------------------------