add working Mercurial commit log parser

Tue, 18 Jul 2023 18:05:49 +0200

author
Mike Becker <universe@uap-core.de>
date
Tue, 18 Jul 2023 18:05:49 +0200
changeset 280
12b898531d1a
parent 279
d73537b925af
child 281
c15b9555ecf3

add working Mercurial commit log parser

fixes #274

src/main/kotlin/de/uapcore/lightpit/vcs/CommitRef.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/vcs/HgConnector.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/vcs/VcsConnector.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/vcs/VcsConnectorResult.kt file | annotate | diff | comparison | revisions
src/test/kotlin/de/uapcore/lightpit/vcs/HgConnectorTest.kt file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/00changelog.i file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/cache/branch2-served file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/cache/rbc-names-v1 file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/cache/rbc-revs-v1 file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/cache/tags2-visible file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/dirstate file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/last-message.txt file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/requires file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/store/00changelog.i file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/store/00manifest.i file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/store/data/another-file.i file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/store/data/testfile.i file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/store/fncache file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/store/phaseroots file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/store/requires file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/store/undo file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/store/undo.backup.fncache file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/store/undo.backupfiles file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/store/undo.phaseroots file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/undo.backup.dirstate file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/undo.bookmarks file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/undo.branch file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/undo.desc file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/undo.dirstate file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/wcache/checkisexec file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/wcache/checklink file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/wcache/checklink-target file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/wcache/checknoexec file | annotate | diff | comparison | revisions
src/test/resources/test-repo/hg/wcache/manifestfulltextcache file | annotate | diff | comparison | revisions
src/test/resources/test-repo/testfile file | annotate | diff | comparison | revisions
--- /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
Binary file src/test/resources/test-repo/hg/00changelog.i has changed
--- /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
Binary file src/test/resources/test-repo/hg/cache/rbc-revs-v1 has changed
--- /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
Binary file src/test/resources/test-repo/hg/dirstate has changed
--- /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
Binary file src/test/resources/test-repo/hg/store/00changelog.i has changed
Binary file src/test/resources/test-repo/hg/store/00manifest.i has changed
Binary file src/test/resources/test-repo/hg/store/data/another-file.i has changed
Binary file src/test/resources/test-repo/hg/store/data/testfile.i has changed
--- /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
Binary file src/test/resources/test-repo/hg/store/undo has changed
--- /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
Binary file src/test/resources/test-repo/hg/store/undo.backupfiles has changed
--- /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
Binary file src/test/resources/test-repo/hg/undo.backup.dirstate has changed
--- /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
Binary file src/test/resources/test-repo/hg/undo.dirstate has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/test/resources/test-repo/hg/wcache/checklink	Tue Jul 18 18:05:49 2023 +0200
@@ -0,0 +1,1 @@
+checklink-target
\ No newline at end of file
Binary file src/test/resources/test-repo/hg/wcache/manifestfulltextcache has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/test/resources/test-repo/testfile	Tue Jul 18 18:05:49 2023 +0200
@@ -0,0 +1,1 @@
+Just a test file.

mercurial