Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: reimplementation of git-standup with Kotlin Native #100

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ build/
local.properties
users.db/
buildscan.log
*.hprof
4 changes: 0 additions & 4 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ allprojects {
mavenCentral()
google()
jcenter()
maven("https://dl.bintray.com/kotlin/kotlin-eap/")
maven("https://kotlin.bintray.com/kotlinx/")
maven("https://dl.bintray.com/kodein-framework/Kodein-DB") // TODO: Remove when Kodein DB exits beta
maven("https://dl.bintray.com/jetbrains/markdown")
}

tasks.withType<KotlinCompile> {
Expand Down
3 changes: 3 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
kotlin.code.style=official
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

kotlin.mpp.stability.nowarn=true
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
14 changes: 14 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
## See https://github.com/casey/just
## Install with $ brew install just
test:
./gradlew :kotlin-testing:test
run:
./gradlew :kotlin-jvm:run
github:
open https://github.com/LouisCAD/kotlin-libraries-playground/
issues:
open https://github.com/LouisCAD/kotlin-libraries-playground/issues
prs:
open https://github.com/LouisCAD/kotlin-libraries-playground/pulls
urls: github issues prs
echo "URLs opened"
1 change: 1 addition & 0 deletions kotlin-cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/nativeMain/resources/git-standup.sh
91 changes: 91 additions & 0 deletions kotlin-cli/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
plugins {
java
kotlin("multiplatform")
}

group = "cli"
version = "1.0-SNAPSHOT"

repositories {
mavenCentral()
}

dependencies {
testImplementation(Testing.junit.params)
testRuntimeOnly(Testing.junit.engine)
}


kotlin {
val hostOs = System.getProperty("os.name")
val isMingwX64 = hostOs.startsWith("Windows")
val nativeTarget = when {
hostOs == "Mac OS X" -> macosX64("native")
hostOs == "Linux" -> linuxX64("native")
isMingwX64 -> mingwX64("native")
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
}

jvm("desktop") {

}

nativeTarget.apply {
binaries {
executable {
entryPoint = "main"
}
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("com.github.ajalt.clikt:clikt:_")
}
}
val commonTest by getting {
dependencies {
implementation(Kotlin.test.common)
implementation(Kotlin.test.annotationsCommon)
}
}
val desktopMain by getting {
dependsOn(commonMain)
}
val desktopTest by getting {
dependencies {

}
}
val nativeMain by getting {
dependsOn(commonMain)

}
val nativeTest by getting {
}

}
}

tasks.getByName<Test>("test") {
useJUnitPlatform()
}

tasks.register<Copy>("install") {
val destDir = "/usr/local/bin"
dependsOn("runDebugExecutableNative")
from("build/bin/native/debugExecutable") {
rename { "git-standup" }
}
into(destDir)
doLast {
println("$ git-standup installed into $destDir")
}
}


// TODO: copy to /usr/local/share/bash-completion/completions
tasks.register<Exec>("dq") {
val completeCommand = "standup --generate-completion zsh > completion-standup.zsh"
commandLine = completeCommand.split(" ")
}
143 changes: 143 additions & 0 deletions kotlin-cli/src/commonMain/kotlin/cli/CliCommand.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package cli

import cli.CliConfig.COMMAND_NAME
import cli.CliConfig.CURRENT_GIT_USER
import cli.CliConfig.FIND
import cli.CliConfig.GIT
import cli.CliConfig.GIT_STANDUP_WHITELIST
import com.github.ajalt.clikt.completion.completionOption
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.int
import io.fileIsReadable
import io.readAllText

class CliCommand : CliktCommand(
help = """
Recall what you did on the last working day ..or be nosy and find what someone else did.
""".trimIndent(),
epilog = """
Repositories will be searched in the current directory
unless a file `.git-standup-whitelist` is found that contains repository paths.

Examples:
$COMMAND_NAME -a "John Doe" -w "MON-FRI" -m 3
""".trimIndent(),
name = COMMAND_NAME
) {
init {
completionOption()
}

val authorOpt: String by option("--author", "-a", help = "Specify author to restrict search to").default("me")
val branch: String by option(
"--branch",
"-b",
help = "Specify branch to restrict search to (unset: all branches, \"\$remote/\$branch\" to include fetches)"
).default("")
val `week-day`: String by option("--week-day", "-w", help = "Specify weekday range to limit search to").default("")
val depth: Int by option("--depth", "-m", help = "Specify the depth of recursive directory search").int()
.default(-1)
val `force-recursion` by option("--force-recursion", "-F", help = "Force recursion up to speficied depth")
val `symbolic-links` by option(
"--symbolic-links",
"-L",
help = "Toggle inclusion of symbolic links in recursive directory search"
).flag(default = false, defaultForHelp = "disabled")
val daysTo: Int by option("-d", "--days", help = "Specify the number of days back to include").int().default(1)
val daysUntil: Int by option("-u", "--until", help = "Specify the number of days back until this day").int()
.default(0)
val `date-format`: String by option(
"-D",
"--date-format",
help = "Specify the number of days back until this day"
).default("")
val help: Boolean by option("-h", "--help", help = "Display this help screen").flag()
val `gpg-signed` by option(
"-g",
"--gpg-signed",
help = "Show if commit is GPG signed (G) or not (N)"
).flag("disabled")
val `fetch` by option("-f", "--fetch", help = "Fetch the latest commits beforehand").flag("--no-fetch")
val `silence` by option(
"-s",
"--silence",
help = "Silences the no activity message (useful when running in a directory having many repositories)"
).flag()
val report by option("-r", "--report", help = "Generate a file with the report").flag()
val `diff-stat` by option("-c", "--diff-stat", help = "Show diffstat for every matched commit")
val afterOpt: String by option("-A", "--after", help = "List commits after this date").default("")
val before: String by option("-B", "--before", help = "List commits before this date").default("")
val `author-date` by option(
"-R",
"--author-date",
help = "Display the author date instead of the committer date"
).flag()
val verbose by option(help = "verbose").flag(defaultForHelp = "disabled")

override fun run() {
if (verbose) println(this)
}

fun gitLogCommand(): List<String> {
val args = mutableListOf<String>()

val branch = if (branch.isBlank()) "--all" else "--first-parent $branch"
val since = if (daysTo == 1) "yesterday" else "$daysTo days ago"
val after = if (afterOpt.isNotBlank()) "--after=$afterOpt" else ""
val gitPrettyDate = if (`author-date`) "%ad" else "%cd"
val gitDateFormat = if (`date-format`.isBlank()) "relative" else `date-format`
val color = "always" // ???
val author = authorName()
var gitPrettyFormat = "'%Cred%h%Creset - %s %Cgreen($gitPrettyDate) %C(bold blue)<%an>%Creset'"
if (`gpg-signed`) gitPrettyFormat += " %C(yellow)gpg: %G?%Creset"
val until = when {
daysUntil != 0 -> "--until='${daysUntil} days ago'"
before.isNotBlank() -> "--until='$before'"
else -> ""
}

args += listOf(GIT, "--no-pager", "log")
args += branch.split(" ")
args += "--no-merges"
args += "--since=$since"
args += until
args += after
args += "--author=$author"
args += "--abbrev-commit"
args += "--oneline"
args += "--pretty=format:$gitPrettyFormat"
args += "--date=$gitDateFormat"
args += "--color=$color"
if (`diff-stat` != null) args += ("--stat")
return args
}

fun authorName() = when (authorOpt) {
"all" -> ".*"
"me" -> CURRENT_GIT_USER
else -> authorOpt
}


fun findCommand(): List<String> {
val args = mutableListOf<String>()

args += FIND
args += when {
fileIsReadable(GIT_STANDUP_WHITELIST) -> readAllText(GIT_STANDUP_WHITELIST).lines().map { it.trim().removeSuffix("/") }
else -> listOf(".")
}
if (`symbolic-links`) args += "-L"
args += "-maxdepth"
args += if (depth == -1) "2" else (depth + 1).toString()
args += "-mindepth"
args += "0"
args += "-name"
args += ".git"
return args.also { if (verbose) println("$ $it") }
}
}

83 changes: 83 additions & 0 deletions kotlin-cli/src/commonMain/kotlin/cli/GitStandup.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package cli

import cli.CliConfig.CURRENT_GIT_USER
import cli.CliConfig.FIND
import cli.CliConfig.GIT
import io.ExecuteCommandOptions
import io.executeCommandAndCaptureOutput
import io.fileIsReadable
import io.findExecutable
import kotlin.native.concurrent.ThreadLocal

@ThreadLocal
object CliConfig {
val COMMAND_NAME = "git-standup"
val GIT_STANDUP_WHITELIST = ".git-standup-whitelist"
var GIT = "git"
var FIND = "find"
var CURRENT_GIT_USER = "me"
}

fun runGitStandup(args: Array<String>) {
val options = ExecuteCommandOptions(directory = ".", abortOnError = true, redirectStderr = true, trim = true)
GIT = findExecutable(GIT)
FIND = findExecutable(FIND)
CURRENT_GIT_USER = executeCommandAndCaptureOutput(listOf(GIT, "config", "user.name"), options)

val command = CliCommand()
val currentDirectory = executeCommandAndCaptureOutput(listOf("pwd"), options).trim()

command.main(args)

if (command.help) {
println(command.getFormattedHelp())
return
}
val gitRepositories =
executeCommandAndCaptureOutput(
command.findCommand(),
options.copy(abortOnError = false, directory = currentDirectory)
)
gitRepositories.lines().filter { it.contains(".git") }.forEach { path ->
val repositoryPath = when {
path.startsWith("./") -> "$currentDirectory/" + path.removePrefix("./")
else -> path
}.removeSuffix(".git").removeSuffix("/")
findCommitsInRepo(repositoryPath, command)
}
}

fun findCommitsInRepo(repositoryPath: String, command: CliCommand) {
val options =
ExecuteCommandOptions(directory = repositoryPath, abortOnError = true, redirectStderr = true, trim = true)

if (fileIsReadable("$repositoryPath/.git").not()) {
if (command.verbose) println("Skipping non-repository with path='$repositoryPath'")
}
if (command.verbose) {
println("findCommitsInRepo($repositoryPath)")
}
// fetch the latest commits if necessary
if (command.fetch) {
val fetchCommand = listOf(GIT, "fetch", "--all")
if (command.verbose) println(fetchCommand)
try {
executeCommandAndCaptureOutput(fetchCommand, options)
} catch (e: Exception) {
println("Warning: could not fetch commits from repository $repositoryPath ; error $e")
}
}

// history
val result = executeCommandAndCaptureOutput(command.gitLogCommand(), options)
if (result.isNotBlank()) {
println("# $repositoryPath")
println(result)
} else if (command.silence.not()) {
println("# $repositoryPath")
println("No commits from ${command.authorName()} during this period")
}
if (command.verbose) {
println("$ " + command.gitLogCommand())
}
}
28 changes: 28 additions & 0 deletions kotlin-cli/src/commonMain/kotlin/io/IO.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io


expect fun readAllText(filePath: String): String

expect fun writeAllText(filePath: String, text: String)

expect fun writeAllLines(
filePath: String,
lines: List<String>
)

expect fun fileIsReadable(filePath: String): Boolean

expect fun executeCommandAndCaptureOutput(
command: List<String>,
options: ExecuteCommandOptions
): String

data class ExecuteCommandOptions(
val directory: String,
val abortOnError: Boolean,
val redirectStderr: Boolean,
val trim: Boolean
)

// call $ which $executable on the JVM
expect fun findExecutable(executable: String): String
Loading