Tue, 18 Jul 2023 18:05:49 +0200
add working Mercurial commit log parser
fixes #274
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/vcs/CommitRef.kt Tue Jul 18 18:05:49 2023 +0200 @@ -0,0 +1,3 @@ +package de.uapcore.lightpit.vcs + +data class CommitRef(val hash: String, val issueId: Int, val message: String)
--- a/src/main/kotlin/de/uapcore/lightpit/vcs/HgConnector.kt Mon Jul 17 14:45:42 2023 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/vcs/HgConnector.kt Tue Jul 18 18:05:49 2023 +0200 @@ -26,29 +26,56 @@ package de.uapcore.lightpit.vcs -import java.util.concurrent.TimeUnit +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.deleteRecursively /** * A connector for Mercurial repositories. * * @param path the path to the Mercurial binary */ -class HgConnector(private val path: String) { +class HgConnector(path: String) : VcsConnector(path) { /** * Checks, if the specified binary is available and executable. */ fun checkAvailability(): Boolean { - return try { - val process = ProcessBuilder(path, "--version").start() - val versionInfo = String(process.inputStream.readAllBytes(), Charsets.UTF_8) - if (process.waitFor(10, TimeUnit.SECONDS)) { - versionInfo.contains("Mercurial") - } else { - false - } - } catch (_: Throwable) { - false + return when (val versionInfo = invokeCommand(Path.of("."), "--version")) { + is VcsConnectorResult.Success -> versionInfo.content.contains("Mercurial") + else -> false } } + + /** + * Reads the commit log and parses every reference to an issue. + * + * The [pathOrUrl] must be a valid path or URL recognized by the VCS binary. + * Currently, no authentication is supported and the repository must therefore be publicly readable. + */ + @OptIn(ExperimentalPathApi::class) + fun readCommitLog(pathOrUrl: String): VcsConnectorResult<List<CommitRef>> { + val tmpDir = try { + Files.createTempDirectory("lightpit-vcs-") + } catch (e: Throwable) { + return VcsConnectorResult.Error("Creating temporary directory for VCS connection failed: " + e.message) + } + val init = invokeCommand(tmpDir, "init") + if (init is VcsConnectorResult.Error) { + return init + } + + val commitLogContent = when (val result = invokeCommand( + tmpDir, "incoming", pathOrUrl, "-n", "--template", "::lpitref::{node}:{desc}\\n" + )) { + is VcsConnectorResult.Error -> return result + is VcsConnectorResult.Success -> result.content + } + + val commitRefs = parseCommitRefs(commitLogContent) + + tmpDir.deleteRecursively() + return VcsConnectorResult.Success(commitRefs) + } } \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/vcs/VcsConnector.kt Tue Jul 18 18:05:49 2023 +0200 @@ -0,0 +1,63 @@ +package de.uapcore.lightpit.vcs + +import java.nio.file.Path +import java.util.concurrent.TimeUnit + +abstract class VcsConnector(protected val path: String) { + /** + * Invokes the VCS binary with the given [args] and returns the output on stdout. + */ + protected fun invokeCommand(workingDir: Path, vararg args : String): VcsConnectorResult<String> { + return try { + val command = mutableListOf(path) + command.addAll(args) + val process = ProcessBuilder(command).directory(workingDir.toFile()).start() + val stdout = String(process.inputStream.readAllBytes(), Charsets.UTF_8) + if (process.waitFor(30, TimeUnit.SECONDS)) { + if (process.exitValue() == 0) { + VcsConnectorResult.Success(stdout) + } else { + VcsConnectorResult.Error("VCS process did not return successfully.") + } + } else { + VcsConnectorResult.Error("VCS process did not return in time.") + } + } catch (e: Throwable) { + VcsConnectorResult.Error("Error during process invocation: "+e.message) + } + } + + /** + * Takes a [commitLog] in format `::lpitref::{node}:{desc}` and parses commit references. + * Supported references are (in this example, 47 is the issue ID): + * - fixes #47 + * - fix #47 + * - closes #47 + * - close #47 + * - relates to #47 + */ + protected fun parseCommitRefs(commitLog: String): List<CommitRef> = buildList { + val marker = "::lpitref::" + var currentHash = "" + var currentDesc = "" + for (line in commitLog.split("\n")) { + // see if current line contains a new log entry + if (line.startsWith(marker)) { + val head = line.substring(marker.length).split(':', limit = 2) + currentHash = head[0] + currentDesc = head[1] + } + + // skip possible preamble output + if (currentHash.isEmpty()) continue + + // scan the lines for commit references + Regex("""(?:relates to|fix(?:es)?|close(?:es)?) #(\d+)""") + .findAll(line) + .map { it.groupValues[1] } + .map { it.toIntOrNull() } + .filterNotNull() + .forEach { commitId -> add(CommitRef(currentHash, commitId, currentDesc)) } + } + } +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/vcs/VcsConnectorResult.kt Tue Jul 18 18:05:49 2023 +0200 @@ -0,0 +1,6 @@ +package de.uapcore.lightpit.vcs + +sealed class VcsConnectorResult<out T> { + data class Success<T>(val content: T) : VcsConnectorResult<T>() + data class Error(val message: String) : VcsConnectorResult<Nothing>() +} \ No newline at end of file
--- a/src/test/kotlin/de/uapcore/lightpit/vcs/HgConnectorTest.kt Mon Jul 17 14:45:42 2023 +0200 +++ b/src/test/kotlin/de/uapcore/lightpit/vcs/HgConnectorTest.kt Tue Jul 18 18:05:49 2023 +0200 @@ -26,13 +26,33 @@ package de.uapcore.lightpit.vcs -import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue +import kotlin.io.path.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.exists +import kotlin.io.path.moveTo +import kotlin.test.* class HgConnectorTest { private val testee = HgConnector("/usr/bin/hg") + private val testRepoPath = Path("src/test/resources/test-repo") + + @BeforeTest + fun prepareTestRepo() { + assertTrue(testRepoPath.exists(), "Test must be run from the project root.") + val hg = testRepoPath.resolve("hg") + val dothg = testRepoPath.resolve(".hg") + assertTrue(hg.exists(), "hg dir not found, maybe a previous execution did not terminated cleanly.") + assertFalse(dothg.exists(), ".hg dir found, maybe a previous execution did not terminated cleanly.") + hg.moveTo(dothg) + } + + @AfterTest + fun cleanup() { + val hg = testRepoPath.resolve("hg") + val dothg = testRepoPath.resolve(".hg") + dothg.moveTo(hg) + } @Test fun checkAvailability() { @@ -43,4 +63,25 @@ fun checkAvailabilityFalse() { assertFalse(HgConnector("/bin/false").checkAvailability()) } + + @Test + fun readCommitLog() { + val result = testee.readCommitLog(testRepoPath.absolutePathString()) + assertTrue(result is VcsConnectorResult.Success) + + assertContentEquals( + listOf( + CommitRef("cf9f5982ddeb28c7f695dc547fe73abf5460016f", 50, "here we fix #50"), + CommitRef("cf9f5982ddeb28c7f695dc547fe73abf5460016f", 30, "here we fix #50"), + CommitRef( + "ed7134e5f4ce278c4f62798fb9f96129be2b132b", + 1337, + "commit with a #non-ref, relates to #wrong ref but still fixes #1337" + ), + CommitRef("74d770da3c80c0c3fc1fb7e44fb710d665127dfe", 47, "a change with commitref in body"), + CommitRef("9a14e5628bdf2d578f3385d78022ddcaf23d1abb", 47, "add test file - relates to #47") + ), + result.content + ) + } } \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/test/resources/test-repo/hg/cache/branch2-served Tue Jul 18 18:05:49 2023 +0200 @@ -0,0 +1,2 @@ +cf9f5982ddeb28c7f695dc547fe73abf5460016f 4 +cf9f5982ddeb28c7f695dc547fe73abf5460016f o default
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/test/resources/test-repo/hg/cache/rbc-names-v1 Tue Jul 18 18:05:49 2023 +0200 @@ -0,0 +1,1 @@ +default \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/test/resources/test-repo/hg/cache/tags2-visible Tue Jul 18 18:05:49 2023 +0200 @@ -0,0 +1,1 @@ +4 cf9f5982ddeb28c7f695dc547fe73abf5460016f
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/test/resources/test-repo/hg/last-message.txt Tue Jul 18 18:05:49 2023 +0200 @@ -0,0 +1,4 @@ +here we fix #50 + +and close #30 +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/test/resources/test-repo/hg/requires Tue Jul 18 18:05:49 2023 +0200 @@ -0,0 +1,1 @@ +share-safe
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/test/resources/test-repo/hg/store/fncache Tue Jul 18 18:05:49 2023 +0200 @@ -0,0 +1,2 @@ +data/testfile.i +data/another-file.i
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/test/resources/test-repo/hg/store/phaseroots Tue Jul 18 18:05:49 2023 +0200 @@ -0,0 +1,1 @@ +1 9a14e5628bdf2d578f3385d78022ddcaf23d1abb
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/test/resources/test-repo/hg/store/requires Tue Jul 18 18:05:49 2023 +0200 @@ -0,0 +1,7 @@ +dotencode +fncache +generaldelta +revlog-compression-zstd +revlogv1 +sparserevlog +store
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/test/resources/test-repo/hg/store/undo.backup.fncache Tue Jul 18 18:05:49 2023 +0200 @@ -0,0 +1,1 @@ +data/testfile.i
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/test/resources/test-repo/hg/store/undo.phaseroots Tue Jul 18 18:05:49 2023 +0200 @@ -0,0 +1,1 @@ +1 9a14e5628bdf2d578f3385d78022ddcaf23d1abb
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/test/resources/test-repo/hg/undo.branch Tue Jul 18 18:05:49 2023 +0200 @@ -0,0 +1,1 @@ +default \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/test/resources/test-repo/hg/undo.desc Tue Jul 18 18:05:49 2023 +0200 @@ -0,0 +1,2 @@ +4 +commit