Skip to content
This repository was archived by the owner on Jul 5, 2025. It is now read-only.

Commit e016912

Browse files
authored
CLI Commands (#6)
2 parents db48d60 + 4412441 commit e016912

File tree

16 files changed

+443
-112
lines changed

16 files changed

+443
-112
lines changed

build.gradle.kts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,28 @@ kotlin {
4343

4444
val commonMain by getting {
4545
dependencies {
46+
val ktorVersion = "2.3.+"
4647
implementation(kotlin("stdlib-common"))
4748
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.+")
4849
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.+")
4950
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.+")
50-
implementation("io.ktor:ktor-client-core:2.3.+")
51-
implementation("io.ktor:ktor-client-auth:2.3.+")
52-
implementation("io.ktor:ktor-client-logging:2.3.+")
53-
implementation("io.ktor:ktor-client-serialization:2.3.+")
54-
implementation("io.ktor:ktor-client-content-negotiation:2.3.+")
55-
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.+")
5651
implementation("com.squareup.okio:okio:3.4.+")
5752
implementation("net.mamoe.yamlkt:yamlkt:0.+")
5853
implementation("com.apollographql.apollo3:apollo-api:4.+")
5954
implementation("com.apollographql.apollo3:apollo-runtime:4.+")
55+
implementation("com.github.ajalt.clikt:clikt:4.2.+")
56+
implementation("io.ktor:ktor-client-core:$ktorVersion")
57+
implementation("io.ktor:ktor-client-auth:$ktorVersion")
58+
implementation("io.ktor:ktor-client-logging:$ktorVersion")
59+
implementation("io.ktor:ktor-client-serialization:$ktorVersion")
60+
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
61+
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
62+
implementation("io.ktor:ktor-server-core:$ktorVersion")
63+
implementation("io.ktor:ktor-server-host-common:$ktorVersion")
64+
implementation("io.ktor:ktor-server-cio:$ktorVersion")
65+
implementation("io.ktor:ktor-server-cors:$ktorVersion")
66+
implementation("io.ktor:ktor-server-forwarded-header:$ktorVersion")
67+
implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
6068
}
6169
}
6270

@@ -65,6 +73,7 @@ kotlin {
6573
implementation(kotlin("test"))
6674
implementation("com.squareup.okio:okio-fakefilesystem:3.4.+")
6775
implementation("com.willowtreeapps.assertk:assertk:0.+")
76+
implementation("io.ktor:ktor-server-test-host:2.3.+")
6877
}
6978
}
7079

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ org.gradle.jvmargs=-Xmx4096m
99
# Project properties
1010
config.group = xyz.marinkovic.milos
1111
config.artifact = codestats
12-
config.version = 0.5.0
12+
config.version = 0.6.0
1313
config.gitHubRepoOwner = milosmns
1414
config.gitHubRepoName = code-stats

src/commonMain/kotlin/Main.kt

Lines changed: 22 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,26 @@
1-
import calculator.di.provideGenericLongMetricCalculators
1+
import commands.CommandLineArguments
2+
import commands.CommandLineArguments.Mode.FETCH
3+
import commands.CommandLineArguments.Mode.PRINT
4+
import commands.CommandLineArguments.Mode.PURGE
5+
import commands.CommandLineArguments.Mode.REPORT
6+
import commands.CommandLineArguments.Mode.SERVE
7+
import commands.di.provideFetchCommand
8+
import commands.di.providePrintCommand
9+
import commands.di.providePurgeCommand
10+
import commands.di.provideReportCommand
11+
import commands.di.provideServeCommand
212
import components.data.TeamHistoryConfig
3-
import history.TeamHistory
4-
import history.filter.transform.RepositoryDateTransform
5-
import history.github.di.provideGitHubHistory
6-
import history.github.di.provideGitHubHistoryConfig
7-
import history.storage.di.provideStoredHistory
8-
import kotlinx.coroutines.runBlocking
9-
import kotlinx.datetime.LocalDate
1013
import utils.fromFile
1114

12-
fun main(): Unit = runBlocking {
13-
/*************************************************************************
14-
THESE ARE TEMPORARY EXPERIMENTS, NOT PART OF THE FINAL PRODUCT
15-
*************************************************************************/
16-
17-
println("\n== Code Stats CLI ==\n")
18-
19-
print("Loading configuration... ")
20-
val teamHistoryConfig = TeamHistoryConfig.fromFile("src/commonMain/resources/sample.config.yaml")
21-
println("Done.")
22-
23-
println(teamHistoryConfig.simpleFormat)
24-
25-
val history: TeamHistory = provideGitHubHistory(
26-
teamHistoryConfig = teamHistoryConfig,
27-
gitHubHistoryConfig = provideGitHubHistoryConfig(),
28-
)
29-
30-
try {
31-
// NETWORK EXPERIMENTS
32-
// println("Loading team history...")
33-
// val fetched = mutableListOf<Repository>()
34-
// teamHistoryConfig.teams.forEach { team ->
35-
// println("Loading for team ${team.title}...")
36-
// team.discussionRepositories.forEach { repoName ->
37-
// println("Loading discussion repository $repoName...")
38-
// fetched += history.fetchRepository(repoName, includeCodeReviews = false, includeDiscussions = true)
39-
// }
40-
// team.codeRepositories.forEach { repoName ->
41-
// println("Loading code repository $repoName...")
42-
// fetched += history.fetchRepository(repoName, includeCodeReviews = true, includeDiscussions = false)
43-
// }
44-
// }
45-
46-
// STORAGE EXPERIMENTS
47-
val storage = provideStoredHistory(teamHistoryConfig)
48-
// storage.purgeAll()
49-
// val storedUnsorted = mutableListOf<Repository>()
50-
// fetched.forEach {
51-
// storage.storeRepositoryDeep(it)
52-
// storedUnsorted += storage.fetchRepository(
53-
// it.name,
54-
// includeCodeReviews = true,
55-
// includeDiscussions = true,
56-
// )
57-
// }
58-
59-
val stored = storage.fetchAllRepositories().map {
60-
storage.fetchRepository(
61-
it.name,
62-
includeCodeReviews = true,
63-
includeDiscussions = true,
64-
)
65-
}
66-
67-
stored.forEach { repo ->
68-
println(repo.simpleFormat)
69-
repo.codeReviews.forEach {
70-
println("\t r#${it.number} ${it.createdAt} >> ${it.mergedAt} >> ${it.closedAt}")
71-
}
72-
repo.discussions.forEach {
73-
println("\t d#${it.number} ${it.createdAt} >> ${it.closedAt}")
74-
}
75-
println("-- ${repo.fullName} --\n")
76-
}
77-
78-
println("Filter by date? DD.MM.YYYY (empty for no filter)")
79-
val dateString = readln().trim()
80-
val filtered = if (dateString.isNotEmpty()) {
81-
val day = dateString.substringBefore(".").toInt()
82-
val month = dateString.substringAfter(".").substringBefore(".").toInt()
83-
val year = dateString.substringAfterLast(".").toInt()
84-
val date = LocalDate(year, month, day)
85-
val transform = RepositoryDateTransform(date, date)
86-
stored.map(transform)
87-
} else stored
88-
89-
// OTHER EXPERIMENTS
90-
provideGenericLongMetricCalculators().forEach {
91-
val metric = it.calculate(filtered)
92-
println(metric.simpleFormat)
93-
println("-- ${metric.name} --\n")
94-
}
95-
} catch (e: Throwable) {
96-
println("CRITICAL FAILURE! \n\n * ${e.message} * \n\n")
97-
e.printStackTrace()
98-
} finally {
99-
history.close()
100-
}
101-
15+
fun main(args: Array<String>) {
16+
val arguments = CommandLineArguments().load(args)
17+
val teamHistoryConfig = TeamHistoryConfig.fromFile(arguments.configFile)
18+
19+
when (arguments.mode) {
20+
SERVE -> provideServeCommand(teamHistoryConfig)
21+
FETCH -> provideFetchCommand(teamHistoryConfig)
22+
REPORT -> provideReportCommand(teamHistoryConfig)
23+
PRINT -> providePrintCommand(teamHistoryConfig)
24+
PURGE -> providePurgeCommand(teamHistoryConfig)
25+
}.run()
10226
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package commands
2+
3+
import com.github.ajalt.clikt.core.CliktCommand
4+
import com.github.ajalt.clikt.parameters.options.default
5+
import com.github.ajalt.clikt.parameters.options.help
6+
import com.github.ajalt.clikt.parameters.options.option
7+
import com.github.ajalt.clikt.parameters.types.enum
8+
import commands.CommandLineArguments.Mode.REPORT
9+
10+
class CommandLineArguments : CliktCommand(
11+
name = "codestats",
12+
help = "Code Stats Utility. Run with --help for more.",
13+
) {
14+
15+
enum class Mode { SERVE, FETCH, REPORT, PRINT, PURGE }
16+
17+
val mode: Mode by option()
18+
.enum<Mode>(ignoreCase = true) { it.name.lowercase() }
19+
.default(REPORT)
20+
.help(
21+
listOf(
22+
"[Serve] launches a server and prints the access URL",
23+
"[Fetch] fetches fresh git data and overwrites the stored history",
24+
"[Report] a short report on what data is available",
25+
"[Print] calculates and prints the code stats to stdout",
26+
"[Purge] deletes all previously stored data",
27+
).joinToString(". ")
28+
)
29+
30+
val configFile: String by option()
31+
.default("src/commonMain/resources/sample.config.yaml")
32+
.help("Path to the configuration YAML file")
33+
34+
override fun run() = Unit
35+
36+
fun load(args: Array<String>) = apply { main(args) }
37+
38+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package commands.cli
2+
3+
import components.data.Repository
4+
import components.data.TeamHistoryConfig
5+
import history.TeamHistory
6+
import history.storage.StoredHistory
7+
import history.storage.config.StorageConfig
8+
import kotlinx.coroutines.Runnable
9+
import kotlinx.coroutines.runBlocking
10+
11+
class FetchCommand(
12+
private val teamHistoryConfig: TeamHistoryConfig,
13+
private val teamHistory: TeamHistory,
14+
private val storageConfig: StorageConfig,
15+
private val storedHistory: StoredHistory,
16+
) : Runnable {
17+
18+
override fun run() = runBlocking {
19+
println("== File configuration ==")
20+
println(teamHistoryConfig.simpleFormat)
21+
22+
println("\n== History fetch ==")
23+
val fetched = mutableListOf<Repository>()
24+
try {
25+
print("This will start a new data fetch and may impact rate limiting. Continue? (y/n) ")
26+
if (readln().lowercase().trim() != "y") {
27+
println("Aborted. No requests were executed.")
28+
return@runBlocking
29+
}
30+
teamHistoryConfig.teams.forEach { team ->
31+
println("Looking into team ${team.title}...")
32+
team.discussionRepositories.forEach {
33+
println("Fetching discussion history for $it...")
34+
fetched += teamHistory.fetchRepository(
35+
repository = it,
36+
includeCodeReviews = false,
37+
includeDiscussions = true,
38+
)
39+
}
40+
team.codeRepositories.forEach {
41+
println("Fetching code/review history for $it...")
42+
fetched += teamHistory.fetchRepository(
43+
repository = it,
44+
includeCodeReviews = true,
45+
includeDiscussions = false,
46+
)
47+
}
48+
}
49+
println("Done. Fetched ${fetched.size} repositories.")
50+
} catch (t: Throwable) {
51+
println("Failed to complete the task. Error: ${t.message}")
52+
return@runBlocking
53+
} finally {
54+
teamHistory.close()
55+
}
56+
57+
println("\n== History storage ==")
58+
print("Storing fetched data will overwrite existing data. Continue? (y/n) ")
59+
if (readln().lowercase().trim() != "y") {
60+
println("Aborted. If rate limiter is applied to your git remote, wait before trying again.")
61+
return@runBlocking
62+
}
63+
println("Now storing ${fetched.size} repositories locally...")
64+
fetched.forEach(storedHistory::storeRepositoryDeep)
65+
println("\nDone. Your fetched data is at ${storageConfig.databasePath}")
66+
}
67+
68+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package commands.cli
2+
3+
import calculator.di.provideGenericLongMetricCalculators
4+
import components.data.TeamHistoryConfig
5+
import history.filter.transform.RepositoryDateTransform
6+
import history.storage.StoredHistory
7+
import kotlinx.coroutines.Runnable
8+
9+
class PrintCommand(
10+
private val teamHistoryConfig: TeamHistoryConfig,
11+
private val storedHistory: StoredHistory,
12+
) : Runnable {
13+
14+
override fun run() {
15+
println("== File configuration ==")
16+
println(teamHistoryConfig.simpleFormat)
17+
18+
println("\n== Metrics ==")
19+
val storedRepos = storedHistory.fetchAllRepositories().map {
20+
storedHistory.fetchRepository(
21+
name = it.name,
22+
includeCodeReviews = true,
23+
includeDiscussions = true,
24+
)
25+
}
26+
val transform = RepositoryDateTransform(teamHistoryConfig.startDate, teamHistoryConfig.endDate)
27+
val filteredRepos = storedRepos.map(transform)
28+
.filter { repo -> repo.codeReviews.isNotEmpty() || repo.discussions.isNotEmpty() }
29+
print("Print details with date filtering applied? (y/n) ")
30+
val dataSet = if (readln().lowercase().trim() == "y") filteredRepos else storedRepos
31+
println("OK.\n")
32+
33+
provideGenericLongMetricCalculators().forEach { calculator ->
34+
val metric = calculator.calculate(dataSet)
35+
println(metric.simpleFormat)
36+
println("-- ${metric.name} --\n")
37+
}
38+
}
39+
40+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package commands.cli
2+
3+
import components.data.TeamHistoryConfig
4+
import history.storage.StoredHistory
5+
import kotlinx.coroutines.Runnable
6+
7+
class PurgeCommand(
8+
private val teamHistoryConfig: TeamHistoryConfig,
9+
private val storedHistory: StoredHistory,
10+
) : Runnable {
11+
12+
override fun run() {
13+
println("== File configuration ==")
14+
println(teamHistoryConfig.simpleFormat)
15+
16+
println("\n== Purge ==")
17+
println("Purging will permanently delete all locally stored data.")
18+
print("There is no data recovery. Continue? (y/n) ")
19+
if (readln().lowercase().trim() != "y") {
20+
println("Aborted. No data was deleted.")
21+
return
22+
}
23+
println("Now purging all locally stored data...")
24+
storedHistory.purgeAll()
25+
println("Done.")
26+
}
27+
28+
}

0 commit comments

Comments
 (0)