Repositories / gitweb2.git
gitweb2.git
Clone (read-only): git clone http://git.guha-anderson.com/git/gitweb2.git
@@ -6,6 +6,8 @@ group = "gitweb" version = "0.1.0" val ktorVersion = "3.4.2" +val okioVersion = "3.17.0" +val processVersion = "0.5.0" val kotlinGitDir = providers.gradleProperty("kotlinGitDir") .orElse("/home/arjun/repos/homebox/kotlin-git") val kotlinGitKlib = kotlinGitDir.map { file("$it/.build/klib/kotlin-git.klib") } @@ -35,6 +37,7 @@ kotlin { all { linkerOpts("-L${pkgConfigValue("--variable=libdir", "libgit2")}") linkerOpts(pkgConfig("--libs", "libgit2")) + linkerOpts("-lutil") } executable { baseName = "gitweb2" @@ -49,6 +52,8 @@ kotlin { dependencies { implementation("io.ktor:ktor-server-core:$ktorVersion") implementation("io.ktor:ktor-server-cio:$ktorVersion") + implementation("com.squareup.okio:okio:$okioVersion") + implementation("io.matthewnelson.kmp-process:process:$processVersion") implementation(files(kotlinGitKlib)) implementation(files(libgit2Klib)) }
@@ -1,5 +1,3 @@ -@file:OptIn(ExperimentalForeignApi::class) - package gitweb import io.ktor.http.ContentType @@ -14,13 +12,7 @@ import io.ktor.server.response.respondText import io.ktor.server.routing.get import io.ktor.server.routing.routing import io.ktor.utils.io.charsets.Charsets -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.ByteVar -import kotlinx.cinterop.allocArray -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.pointed -import kotlinx.cinterop.staticCFunction -import kotlinx.cinterop.toKString +import io.matthewnelson.kmp.process.Process as KmpProcess import kotlinx.git.Blob import kotlinx.git.Branch import kotlinx.git.BranchType @@ -28,20 +20,8 @@ import kotlinx.git.Git import kotlinx.git.GitException import kotlinx.git.Repository import kotlinx.git.TreeEntryKind -import platform.posix.F_OK -import platform.posix.SIGINT -import platform.posix.SIGTERM -import platform.posix.access -import platform.posix.closedir -import platform.posix.fgets -import platform.posix.getenv -import platform.posix.opendir -import platform.posix.pclose -import platform.posix.pause -import platform.posix.popen -import platform.posix.readdir -import platform.posix.signal -import platform.posix._exit +import okio.FileSystem +import okio.Path.Companion.toPath import kotlin.system.exitProcess private const val DefaultHost = "127.0.0.1" @@ -86,9 +66,7 @@ internal class GitWebServer( } } try { - server.start(wait = false) - installSignalHandlers() - while (true) pause() + server.start(wait = true) } finally { server.stop() Git.shutdown() @@ -276,15 +254,6 @@ internal class GitWebServer( private data class RepoSummary(val branch: String, val summary: String, val shortId: String) } -private fun installSignalHandlers() { - signal(SIGINT, staticCFunction(::exitFromSignal)) - signal(SIGTERM, staticCFunction(::exitFromSignal)) -} - -private fun exitFromSignal(signalNumber: Int) { - _exit(128 + signalNumber) -} - private fun page(title: String, body: String): String = """ <!doctype html> <html lang="en"> @@ -342,10 +311,11 @@ internal class FileRenderer(private val pygmentsCommand: String = DefaultPygment renderWithPygments(filePath, text) ?: plain(text) private fun renderWithPygments(filePath: String, text: String): String? { + if (!commandExists(pygmentsCommand)) return null val lexer = readCommandOutput("$pygmentsCommand -N ${shellQuote(filePath)} 2>/dev/null")?.trim() val lexerOption = if (lexer.isNullOrBlank()) "-g" else "-l ${shellQuote(lexer)}" - val command = "printf %s ${shellQuote(text)} | $pygmentsCommand -f html -O nowrap,noclasses=True $lexerOption 2>/dev/null" - val highlighted = readCommandOutput(command) ?: return null + val command = "$pygmentsCommand -f html -O nowrap,noclasses=True $lexerOption 2>/dev/null" + val highlighted = readCommandOutput(command, text) ?: return null if (highlighted.isBlank()) return null return "<div class=\"highlight\">$highlighted</div>" } @@ -363,24 +333,30 @@ private fun isReadme(name: String): Boolean = private fun shellQuote(value: String): String = "'" + value.replace("'", "'\"'\"'") + "'" -private fun readCommandOutput(command: String): String? { - val stream = popen(command, "r") ?: return null - return try { - memScoped { - val buffer = allocArray<ByteVar>(4096) - buildString { - while (true) { - val line = fgets(buffer, 4096, stream) ?: break - append(line.toKString()) - } +private fun commandExists(command: String): Boolean { + val executable = command.trim().substringBefore(' ') + if (executable.isBlank()) return false + if ('/' in executable) return pathExists(executable) + return KmpProcess.Current.environment()["PATH"] + ?.split(':') + ?.any { pathExists(joinPath(it, executable)) } == true +} + +private fun readCommandOutput(command: String, input: String? = null): String? { + val output = runCatching { + KmpProcess.Builder("/bin/sh") + .args("-c", command) + .createOutput { + timeoutMillis = 10_000 + if (input != null) inputUtf8 { input } } - } - } finally { - pclose(stream) - } + }.getOrNull() ?: return null + if (output.processError != null || output.processInfo.exitCode != 0) return null + return output.stdout.replace("\r\n", "\n").replace('\r', '\n') } private fun discoverRepos(root: String): List<RepoInfo> { + val fileSystem = FileSystem.SYSTEM val found = mutableListOf<RepoInfo>() fun scan(path: String, relative: String, depth: Int) { if (isGitRepo(path)) { @@ -391,18 +367,13 @@ private fun discoverRepos(root: String): List<RepoInfo> { return } if (depth >= MaxScanDepth) return - val dir = opendir(path) ?: return - try { - while (true) { - val entry = readdir(dir) ?: break - val name = entry.pointed.d_name.toKString() - if (name == "." || name == ".." || name == ".git") continue - val child = joinPath(path, name) - val childRelative = if (relative.isBlank()) name else "$relative/$name" - scan(child, childRelative, depth + 1) - } - } finally { - closedir(dir) + val entries = runCatching { fileSystem.list(path.toPath()) }.getOrElse { return } + for (entry in entries) { + val name = entry.name + if (name == "." || name == ".." || name == ".git") continue + val child = entry.toString() + val childRelative = if (relative.isBlank()) name else "$relative/$name" + scan(child, childRelative, depth + 1) } } scan(normalizeRoot(root), "", 0) @@ -413,10 +384,13 @@ private fun Repository.localBranches(): List<Branch> = branches().filter { it.type == BranchType.Local } private fun isGitRepo(path: String): Boolean = - access(joinPath(path, ".git"), F_OK) == 0 || - access(joinPath(path, "HEAD"), F_OK) == 0 && - access(joinPath(path, "objects"), F_OK) == 0 && - access(joinPath(path, "refs"), F_OK) == 0 + pathExists(joinPath(path, ".git")) || + pathExists(joinPath(path, "HEAD")) && + pathExists(joinPath(path, "objects")) && + pathExists(joinPath(path, "refs")) + +private fun pathExists(path: String): Boolean = + FileSystem.SYSTEM.metadataOrNull(path.toPath()) != null private fun isOpenableGitRepo(path: String): Boolean = runCatching { Git.open(path).use { } }.isSuccess @@ -485,21 +459,23 @@ private fun urlDecode(value: String): String { } private fun expandHome(path: String): String { - if (path == "~") return getenv("HOME")?.toKString() ?: path - if (path.startsWith("~/")) return "${getenv("HOME")?.toKString() ?: ""}/${path.drop(2)}" + if (path == "~") return homeDirectory() ?: path + if (path.startsWith("~/")) return "${homeDirectory() ?: return path}/${path.drop(2)}" return path } private fun normalizeRoot(path: String): String { val expanded = expandHome(path) - val cwd = getenv("PWD")?.toKString() - return when { - expanded == "." && cwd != null -> cwd - expanded.startsWith("./") && cwd != null -> joinPath(cwd, expanded.drop(2)) - else -> expanded + return runCatching { + FileSystem.SYSTEM.canonicalize(expanded.toPath()).toString() + }.getOrElse { + expanded }.trimEnd('/') } +private fun homeDirectory(): String? = + KmpProcess.Current.environment()["HOME"]?.takeIf { it.isNotBlank() } + private fun parseArgs(args: Array<String>): Config? { if (args.isEmpty() || args[0] == "-h" || args[0] == "--help") { println("usage: gitweb2 <repo-root> [--host 127.0.0.1] [--port 8080] [--pygments pygmentize]")
@@ -1,15 +1,13 @@ package gitweb -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.toKString -import platform.posix.getenv +import okio.FileSystem +import okio.Path.Companion.toPath import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals -@OptIn(ExperimentalForeignApi::class) class FileRendererTest { - private val repoRoot: String = getenv("PWD")!!.toKString() + private val repoRoot: String = FileSystem.SYSTEM.canonicalize(".".toPath()).toString() @Test fun filesCanBeRenderedThroughCustomPygmentsCommand() {
@@ -1,17 +1,15 @@ package gitweb -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.toKString -import platform.posix.getenv +import okio.FileSystem +import okio.Path.Companion.toPath import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertNotEquals import kotlin.test.assertTrue -@OptIn(ExperimentalForeignApi::class) class GitWebServerTest { - private val repoRoot: String = getenv("PWD")!!.toKString() + private val repoRoot: String = FileSystem.SYSTEM.canonicalize(".".toPath()).toString() private val repoName: String = repoRoot.trimEnd('/').substringAfterLast('/') @Test