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
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/src/main/kotlin/de/uapcore/lightpit/vcs/CommitRef.kt	Tue Jul 18 18:05:49 2023 +0200
     1.3 @@ -0,0 +1,3 @@
     1.4 +package de.uapcore.lightpit.vcs
     1.5 +
     1.6 +data class CommitRef(val hash: String, val issueId: Int, val message: String)
     2.1 --- a/src/main/kotlin/de/uapcore/lightpit/vcs/HgConnector.kt	Mon Jul 17 14:45:42 2023 +0200
     2.2 +++ b/src/main/kotlin/de/uapcore/lightpit/vcs/HgConnector.kt	Tue Jul 18 18:05:49 2023 +0200
     2.3 @@ -26,29 +26,56 @@
     2.4  
     2.5  package de.uapcore.lightpit.vcs
     2.6  
     2.7 -import java.util.concurrent.TimeUnit
     2.8 +import java.nio.file.Files
     2.9 +import java.nio.file.Path
    2.10 +import kotlin.io.path.ExperimentalPathApi
    2.11 +import kotlin.io.path.deleteRecursively
    2.12  
    2.13  /**
    2.14   * A connector for Mercurial repositories.
    2.15   *
    2.16   * @param path the path to the Mercurial binary
    2.17   */
    2.18 -class HgConnector(private val path: String) {
    2.19 +class HgConnector(path: String) : VcsConnector(path) {
    2.20  
    2.21      /**
    2.22       * Checks, if the specified binary is available and executable.
    2.23       */
    2.24      fun checkAvailability(): Boolean {
    2.25 -        return try {
    2.26 -            val process = ProcessBuilder(path, "--version").start()
    2.27 -            val versionInfo = String(process.inputStream.readAllBytes(), Charsets.UTF_8)
    2.28 -            if (process.waitFor(10, TimeUnit.SECONDS)) {
    2.29 -                versionInfo.contains("Mercurial")
    2.30 -            } else {
    2.31 -                false
    2.32 -            }
    2.33 -        } catch (_: Throwable) {
    2.34 -            false
    2.35 +        return when (val versionInfo = invokeCommand(Path.of("."), "--version")) {
    2.36 +            is VcsConnectorResult.Success -> versionInfo.content.contains("Mercurial")
    2.37 +            else -> false
    2.38          }
    2.39      }
    2.40 +
    2.41 +    /**
    2.42 +     * Reads the commit log and parses every reference to an issue.
    2.43 +     *
    2.44 +     * The [pathOrUrl] must be a valid path or URL recognized by the VCS binary.
    2.45 +     * Currently, no authentication is supported and the repository must therefore be publicly readable.
    2.46 +     */
    2.47 +    @OptIn(ExperimentalPathApi::class)
    2.48 +    fun readCommitLog(pathOrUrl: String): VcsConnectorResult<List<CommitRef>> {
    2.49 +        val tmpDir = try {
    2.50 +            Files.createTempDirectory("lightpit-vcs-")
    2.51 +        } catch (e: Throwable) {
    2.52 +            return VcsConnectorResult.Error("Creating temporary directory for VCS connection failed: " + e.message)
    2.53 +        }
    2.54 +        val init = invokeCommand(tmpDir, "init")
    2.55 +        if (init is VcsConnectorResult.Error) {
    2.56 +            return init
    2.57 +        }
    2.58 +
    2.59 +        val commitLogContent = when (val result = invokeCommand(
    2.60 +            tmpDir, "incoming", pathOrUrl, "-n", "--template", "::lpitref::{node}:{desc}\\n"
    2.61 +        )) {
    2.62 +            is VcsConnectorResult.Error -> return result
    2.63 +            is VcsConnectorResult.Success -> result.content
    2.64 +        }
    2.65 +
    2.66 +        val commitRefs = parseCommitRefs(commitLogContent)
    2.67 +
    2.68 +        tmpDir.deleteRecursively()
    2.69 +        return VcsConnectorResult.Success(commitRefs)
    2.70 +    }
    2.71  }
    2.72 \ No newline at end of file
     3.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     3.2 +++ b/src/main/kotlin/de/uapcore/lightpit/vcs/VcsConnector.kt	Tue Jul 18 18:05:49 2023 +0200
     3.3 @@ -0,0 +1,63 @@
     3.4 +package de.uapcore.lightpit.vcs
     3.5 +
     3.6 +import java.nio.file.Path
     3.7 +import java.util.concurrent.TimeUnit
     3.8 +
     3.9 +abstract class VcsConnector(protected val path: String) {
    3.10 +    /**
    3.11 +     * Invokes the VCS binary with the given [args] and returns the output on stdout.
    3.12 +     */
    3.13 +    protected fun invokeCommand(workingDir: Path, vararg args : String): VcsConnectorResult<String> {
    3.14 +        return try {
    3.15 +            val command = mutableListOf(path)
    3.16 +            command.addAll(args)
    3.17 +            val process = ProcessBuilder(command).directory(workingDir.toFile()).start()
    3.18 +            val stdout = String(process.inputStream.readAllBytes(), Charsets.UTF_8)
    3.19 +            if (process.waitFor(30, TimeUnit.SECONDS)) {
    3.20 +                if (process.exitValue() == 0) {
    3.21 +                    VcsConnectorResult.Success(stdout)
    3.22 +                } else {
    3.23 +                    VcsConnectorResult.Error("VCS process did not return successfully.")
    3.24 +                }
    3.25 +            } else {
    3.26 +                VcsConnectorResult.Error("VCS process did not return in time.")
    3.27 +            }
    3.28 +        } catch (e: Throwable) {
    3.29 +            VcsConnectorResult.Error("Error during process invocation: "+e.message)
    3.30 +        }
    3.31 +    }
    3.32 +
    3.33 +    /**
    3.34 +     * Takes a [commitLog] in format `::lpitref::{node}:{desc}` and parses commit references.
    3.35 +     * Supported references are (in this example, 47 is the issue ID):
    3.36 +     *  - fixes #47
    3.37 +     *  - fix #47
    3.38 +     *  - closes #47
    3.39 +     *  - close #47
    3.40 +     *  - relates to #47
    3.41 +     */
    3.42 +    protected fun parseCommitRefs(commitLog: String): List<CommitRef> = buildList {
    3.43 +        val marker = "::lpitref::"
    3.44 +        var currentHash = ""
    3.45 +        var currentDesc = ""
    3.46 +        for (line in commitLog.split("\n")) {
    3.47 +            // see if current line contains a new log entry
    3.48 +            if (line.startsWith(marker)) {
    3.49 +                val head = line.substring(marker.length).split(':', limit = 2)
    3.50 +                currentHash = head[0]
    3.51 +                currentDesc = head[1]
    3.52 +            }
    3.53 +
    3.54 +            // skip possible preamble output
    3.55 +            if (currentHash.isEmpty()) continue
    3.56 +
    3.57 +            // scan the lines for commit references
    3.58 +            Regex("""(?:relates to|fix(?:es)?|close(?:es)?) #(\d+)""")
    3.59 +                .findAll(line)
    3.60 +                .map { it.groupValues[1] }
    3.61 +                .map { it.toIntOrNull() }
    3.62 +                .filterNotNull()
    3.63 +                .forEach { commitId -> add(CommitRef(currentHash, commitId, currentDesc)) }
    3.64 +        }
    3.65 +    }
    3.66 +}
    3.67 \ No newline at end of file
     4.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     4.2 +++ b/src/main/kotlin/de/uapcore/lightpit/vcs/VcsConnectorResult.kt	Tue Jul 18 18:05:49 2023 +0200
     4.3 @@ -0,0 +1,6 @@
     4.4 +package de.uapcore.lightpit.vcs
     4.5 +
     4.6 +sealed class VcsConnectorResult<out T> {
     4.7 +    data class Success<T>(val content: T) : VcsConnectorResult<T>()
     4.8 +    data class Error(val message: String) : VcsConnectorResult<Nothing>()
     4.9 +}
    4.10 \ No newline at end of file
     5.1 --- a/src/test/kotlin/de/uapcore/lightpit/vcs/HgConnectorTest.kt	Mon Jul 17 14:45:42 2023 +0200
     5.2 +++ b/src/test/kotlin/de/uapcore/lightpit/vcs/HgConnectorTest.kt	Tue Jul 18 18:05:49 2023 +0200
     5.3 @@ -26,13 +26,33 @@
     5.4  
     5.5  package de.uapcore.lightpit.vcs
     5.6  
     5.7 -import kotlin.test.Test
     5.8 -import kotlin.test.assertFalse
     5.9 -import kotlin.test.assertTrue
    5.10 +import kotlin.io.path.Path
    5.11 +import kotlin.io.path.absolutePathString
    5.12 +import kotlin.io.path.exists
    5.13 +import kotlin.io.path.moveTo
    5.14 +import kotlin.test.*
    5.15  
    5.16  class HgConnectorTest {
    5.17  
    5.18      private val testee = HgConnector("/usr/bin/hg")
    5.19 +    private val testRepoPath = Path("src/test/resources/test-repo")
    5.20 +
    5.21 +    @BeforeTest
    5.22 +    fun prepareTestRepo() {
    5.23 +        assertTrue(testRepoPath.exists(), "Test must be run from the project root.")
    5.24 +        val hg = testRepoPath.resolve("hg")
    5.25 +        val dothg = testRepoPath.resolve(".hg")
    5.26 +        assertTrue(hg.exists(), "hg dir not found, maybe a previous execution did not terminated cleanly.")
    5.27 +        assertFalse(dothg.exists(), ".hg dir found, maybe a previous execution did not terminated cleanly.")
    5.28 +        hg.moveTo(dothg)
    5.29 +    }
    5.30 +
    5.31 +    @AfterTest
    5.32 +    fun cleanup() {
    5.33 +        val hg = testRepoPath.resolve("hg")
    5.34 +        val dothg = testRepoPath.resolve(".hg")
    5.35 +        dothg.moveTo(hg)
    5.36 +    }
    5.37  
    5.38      @Test
    5.39      fun checkAvailability() {
    5.40 @@ -43,4 +63,25 @@
    5.41      fun checkAvailabilityFalse() {
    5.42          assertFalse(HgConnector("/bin/false").checkAvailability())
    5.43      }
    5.44 +
    5.45 +    @Test
    5.46 +    fun readCommitLog() {
    5.47 +        val result = testee.readCommitLog(testRepoPath.absolutePathString())
    5.48 +        assertTrue(result is VcsConnectorResult.Success)
    5.49 +
    5.50 +        assertContentEquals(
    5.51 +            listOf(
    5.52 +                CommitRef("cf9f5982ddeb28c7f695dc547fe73abf5460016f", 50, "here we fix #50"),
    5.53 +                CommitRef("cf9f5982ddeb28c7f695dc547fe73abf5460016f", 30, "here we fix #50"),
    5.54 +                CommitRef(
    5.55 +                    "ed7134e5f4ce278c4f62798fb9f96129be2b132b",
    5.56 +                    1337,
    5.57 +                    "commit with a #non-ref, relates to #wrong ref but still fixes #1337"
    5.58 +                ),
    5.59 +                CommitRef("74d770da3c80c0c3fc1fb7e44fb710d665127dfe", 47, "a change with commitref in body"),
    5.60 +                CommitRef("9a14e5628bdf2d578f3385d78022ddcaf23d1abb", 47, "add test file - relates to #47")
    5.61 +            ),
    5.62 +            result.content
    5.63 +        )
    5.64 +    }
    5.65  }
    5.66 \ No newline at end of file
     6.1 Binary file src/test/resources/test-repo/hg/00changelog.i has changed
     7.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     7.2 +++ b/src/test/resources/test-repo/hg/cache/branch2-served	Tue Jul 18 18:05:49 2023 +0200
     7.3 @@ -0,0 +1,2 @@
     7.4 +cf9f5982ddeb28c7f695dc547fe73abf5460016f 4
     7.5 +cf9f5982ddeb28c7f695dc547fe73abf5460016f o default
     8.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     8.2 +++ b/src/test/resources/test-repo/hg/cache/rbc-names-v1	Tue Jul 18 18:05:49 2023 +0200
     8.3 @@ -0,0 +1,1 @@
     8.4 +default
     8.5 \ No newline at end of file
     9.1 Binary file src/test/resources/test-repo/hg/cache/rbc-revs-v1 has changed
    10.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    10.2 +++ b/src/test/resources/test-repo/hg/cache/tags2-visible	Tue Jul 18 18:05:49 2023 +0200
    10.3 @@ -0,0 +1,1 @@
    10.4 +4 cf9f5982ddeb28c7f695dc547fe73abf5460016f
    11.1 Binary file src/test/resources/test-repo/hg/dirstate has changed
    12.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    12.2 +++ b/src/test/resources/test-repo/hg/last-message.txt	Tue Jul 18 18:05:49 2023 +0200
    12.3 @@ -0,0 +1,4 @@
    12.4 +here we fix #50
    12.5 +
    12.6 +and close #30
    12.7 +
    13.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    13.2 +++ b/src/test/resources/test-repo/hg/requires	Tue Jul 18 18:05:49 2023 +0200
    13.3 @@ -0,0 +1,1 @@
    13.4 +share-safe
    14.1 Binary file src/test/resources/test-repo/hg/store/00changelog.i has changed
    15.1 Binary file src/test/resources/test-repo/hg/store/00manifest.i has changed
    16.1 Binary file src/test/resources/test-repo/hg/store/data/another-file.i has changed
    17.1 Binary file src/test/resources/test-repo/hg/store/data/testfile.i has changed
    18.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    18.2 +++ b/src/test/resources/test-repo/hg/store/fncache	Tue Jul 18 18:05:49 2023 +0200
    18.3 @@ -0,0 +1,2 @@
    18.4 +data/testfile.i
    18.5 +data/another-file.i
    19.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    19.2 +++ b/src/test/resources/test-repo/hg/store/phaseroots	Tue Jul 18 18:05:49 2023 +0200
    19.3 @@ -0,0 +1,1 @@
    19.4 +1 9a14e5628bdf2d578f3385d78022ddcaf23d1abb
    20.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    20.2 +++ b/src/test/resources/test-repo/hg/store/requires	Tue Jul 18 18:05:49 2023 +0200
    20.3 @@ -0,0 +1,7 @@
    20.4 +dotencode
    20.5 +fncache
    20.6 +generaldelta
    20.7 +revlog-compression-zstd
    20.8 +revlogv1
    20.9 +sparserevlog
   20.10 +store
    21.1 Binary file src/test/resources/test-repo/hg/store/undo has changed
    22.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    22.2 +++ b/src/test/resources/test-repo/hg/store/undo.backup.fncache	Tue Jul 18 18:05:49 2023 +0200
    22.3 @@ -0,0 +1,1 @@
    22.4 +data/testfile.i
    23.1 Binary file src/test/resources/test-repo/hg/store/undo.backupfiles has changed
    24.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    24.2 +++ b/src/test/resources/test-repo/hg/store/undo.phaseroots	Tue Jul 18 18:05:49 2023 +0200
    24.3 @@ -0,0 +1,1 @@
    24.4 +1 9a14e5628bdf2d578f3385d78022ddcaf23d1abb
    25.1 Binary file src/test/resources/test-repo/hg/undo.backup.dirstate has changed
    26.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    26.2 +++ b/src/test/resources/test-repo/hg/undo.branch	Tue Jul 18 18:05:49 2023 +0200
    26.3 @@ -0,0 +1,1 @@
    26.4 +default
    26.5 \ No newline at end of file
    27.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    27.2 +++ b/src/test/resources/test-repo/hg/undo.desc	Tue Jul 18 18:05:49 2023 +0200
    27.3 @@ -0,0 +1,2 @@
    27.4 +4
    27.5 +commit
    28.1 Binary file src/test/resources/test-repo/hg/undo.dirstate has changed
    29.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    29.2 +++ b/src/test/resources/test-repo/hg/wcache/checklink	Tue Jul 18 18:05:49 2023 +0200
    29.3 @@ -0,0 +1,1 @@
    29.4 +checklink-target
    29.5 \ No newline at end of file
    30.1 Binary file src/test/resources/test-repo/hg/wcache/manifestfulltextcache has changed
    31.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    31.2 +++ b/src/test/resources/test-repo/testfile	Tue Jul 18 18:05:49 2023 +0200
    31.3 @@ -0,0 +1,1 @@
    31.4 +Just a test file.

mercurial