Tue, 18 Jul 2023 18:05:49 +0200
add working Mercurial commit log parser
fixes #274
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.