add timestamp to commit references

Tue, 11 Mar 2025 13:59:06 +0100

author
Mike Becker <universe@uap-core.de>
date
Tue, 11 Mar 2025 13:59:06 +0100
changeset 360
f60ecdc0431f
parent 359
842bb8976b0f
child 361
749d71470b0f

add timestamp to commit references

resolves #539

setup/postgres/psql_create_tables.sql file | annotate | diff | comparison | revisions
setup/postgres/psql_patch_1.5.0.sql file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/types/CommitRef.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt file | annotate | diff | comparison | revisions
src/main/resources/localization/strings.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/strings_de.properties file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/changelogs/changelog-de.jspf file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/changelogs/changelog.jspf file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issue-view.jsp file | annotate | diff | comparison | revisions
src/test/kotlin/de/uapcore/lightpit/types/CommitRefTest.kt file | annotate | diff | comparison | revisions
--- a/setup/postgres/psql_create_tables.sql	Sun Mar 09 17:17:59 2025 +0100
+++ b/setup/postgres/psql_create_tables.sql	Tue Mar 11 13:59:06 2025 +0100
@@ -186,9 +186,10 @@
 
 create table lpit_commit_ref
 (
-    issueid      integer not null references lpit_issue (issueid) on delete cascade,
-    commit_hash  text    not null,
-    commit_brief text    not null
+    issueid      integer                  not null references lpit_issue (issueid) on delete cascade,
+    commit_hash  text                     not null,
+    commit_brief text                     not null,
+    commit_time  timestamp with time zone null -- optional feature added with Lightpit 1.5.0
 );
 
 create unique index lpit_commit_ref_unique on lpit_commit_ref (issueid, commit_hash);
--- a/setup/postgres/psql_patch_1.5.0.sql	Sun Mar 09 17:17:59 2025 +0100
+++ b/setup/postgres/psql_patch_1.5.0.sql	Tue Mar 11 13:59:06 2025 +0100
@@ -25,3 +25,5 @@
     outdated  boolean        not null default false,
     primary key (issueid, variant)
 );
+
+alter table lpit_commit_ref add commit_time timestamp with time zone null;
--- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt	Sun Mar 09 17:17:59 2025 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt	Tue Mar 11 13:59:06 2025 +0100
@@ -148,5 +148,9 @@
      * Lists the issue comment history, optionally restricted to [project], for the past [days].
      */
     fun listIssueCommentHistory(project: Project?, days: Int): List<IssueCommentHistoryEntry>
+
+    /**
+     * Lists commit references for the specified [issue] in chronological order, if timestamps are available.
+     */
     fun listCommitRefs(issue: Issue): List<CommitRef>
 }
--- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Sun Mar 09 17:17:59 2025 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Tue Mar 11 13:59:06 2025 +0100
@@ -580,11 +580,19 @@
         }
 
     override fun mergeCommitRefs(refs: List<CommitRef>) {
-        withStatement("insert into lpit_commit_ref (issueid, commit_hash, commit_brief) values (?,?,?) on conflict do nothing") {
+        withStatement(
+            """
+            insert into lpit_commit_ref (issueid, commit_hash, commit_brief, commit_time)
+            values (?,?,?,?)
+            on conflict (issueid, commit_hash)
+            do update set commit_brief = excluded.commit_brief, commit_time = excluded.commit_time
+            """.trimIndent()
+        ) {
             refs.forEach { ref ->
                 setInt(1, ref.issueId)
                 setString(2, ref.hash)
                 setString(3, ref.message)
+                setTimestamp(4, ref.timestamp)
                 executeUpdate()
             }
         }
@@ -985,13 +993,14 @@
     }
 
     override fun listCommitRefs(issue: Issue): List<CommitRef> =
-        withStatement("select commit_hash, commit_brief from lpit_commit_ref where issueid = ?") {
+        withStatement("select commit_hash, commit_brief, commit_time from lpit_commit_ref where issueid = ? order by commit_time") {
             setInt(1, issue.id)
             queryAll {
                 CommitRef(
                     issueId = issue.id,
                     hash = it.getString("commit_hash"),
-                    message = it.getString("commit_brief")
+                    message = it.getString("commit_brief"),
+                    timestamp = it.getTimestamp("commit_time")
                 )
             }
         }
--- a/src/main/kotlin/de/uapcore/lightpit/types/CommitRef.kt	Sun Mar 09 17:17:59 2025 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/types/CommitRef.kt	Tue Mar 11 13:59:06 2025 +0100
@@ -26,10 +26,37 @@
 
 package de.uapcore.lightpit.types
 
-data class CommitRef(val hash: String, val issueId: Int, val message: String)
+import java.sql.Timestamp
+import java.time.Instant
+
+data class CommitRef(val hash: String, val issueId: Int, val message: String, val timestamp: Timestamp? = null)
+
+class CommitRefParserInfo(
+    val marker: String,
+    val delimiter: Char,
+    val posHash: Int,
+    val posDesc: Int,
+    val posTime: Int? = null
+) {
+    val metaFields = if (posTime == null) 2 else 3
+}
+
+object CommitRefParserInfos {
+    /**
+     * Parses commit logs of format `::lpitref:{node}::{desc}\n`.
+     */
+    val v1 = CommitRefParserInfo("::lpitref:", ':', posHash = 0, posDesc = 1)
+    /**
+     * Parses commit logs of format `;;lpitref;{node};{date|rfc3339date};{desc}\n`.
+     *
+     * Note that hg needs to output rfc3339date, because the Java standard library is not conforming with iso8601.
+     * And rfc3339date produces an iso8601-compatible timestamp that Java DateTimeFormatter understands.
+     */
+    val v2 = CommitRefParserInfo(";;lpitref;", ';', posHash = 0, posTime = 1, posDesc = 2)
+}
 
 /**
- * Takes a [commitLog] in format `::lpitref::{node}:{desc}` and parses commit references.
+ * Takes a [commitLog] in a format depending on the version and parses commit references.
  * Supported references are (in this example, 47 is the issue ID):
  *  - fix, fixes, fixed #47
  *  - close, closes, closed #47
@@ -39,19 +66,30 @@
  *  - issue #37
  */
 fun parseCommitRefs(commitLog: String): List<CommitRef> = buildList {
-    val marker = "::lpitref:"
-    var currentHash = ""
-    var currentDesc = ""
+    // peek into first line, to determine the parser version to use
+    val parser = commitLog.trimStart().let {
+        if (it.startsWith(CommitRefParserInfos.v1.marker)) CommitRefParserInfos.v1
+        else if (it.startsWith(CommitRefParserInfos.v2.marker)) CommitRefParserInfos.v2
+        else null
+    }
+    if (parser == null) return@buildList
+
+    var hash = ""
+    var desc = ""
+    var timestamp: Timestamp? = null;
     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]
+        if (line.startsWith(parser.marker)) {
+            val head = line.substring(parser.marker.length).split(parser.delimiter, limit = parser.metaFields)
+            hash = head[parser.posHash]
+            desc = head[parser.posDesc]
+            if (parser.posTime != null) {
+                timestamp = Timestamp.from(Instant.parse(head[parser.posTime]))
+            }
         }
 
         // skip possible preamble output
-        if (currentHash.isEmpty()) continue
+        if (hash.isEmpty()) continue
 
         // scan the lines for commit references
         Regex("""(?:issue|relates? to|fix(?:e[sd])?|(?:close|resolve)[sd]?) #(\d+)""")
@@ -59,7 +97,7 @@
             .map { it.groupValues[1] }
             .map { it.toIntOrNull() }
             .filterNotNull()
-            .forEach { commitId -> addFirst(CommitRef(currentHash, commitId, currentDesc)) }
+            .forEach { commitId -> addFirst(CommitRef(hash, commitId, desc, timestamp)) }
     }
 }
 
--- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt	Sun Mar 09 17:17:59 2025 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt	Tue Mar 11 13:59:06 2025 +0100
@@ -36,6 +36,7 @@
 import de.uapcore.lightpit.entities.*
 import de.uapcore.lightpit.logic.compareEtaTo
 import de.uapcore.lightpit.types.*
+import java.sql.Timestamp
 import kotlin.math.roundToInt
 
 class IssueSorter(private vararg val criteria: Criteria) : Comparator<Issue> {
@@ -101,7 +102,7 @@
     }
 }
 
-data class CommitLink(val url: String, val hash: String, val message: String)
+data class CommitLink(val url: String, val timestamp: Timestamp?, val hash: String, val message: String)
 
 class IssueOverview(
     val issues: List<Issue>,
@@ -150,7 +151,7 @@
 
         val commitBaseUrl = issue.project.repoUrl
         commitLinks = (if (commitBaseUrl == null || issue.project.vcs == VcsType.None) emptyList() else commitRefs.map {
-            CommitLink(buildCommitUrl(commitBaseUrl, issue.project.vcs, it.hash), it.hash, it.message)
+            CommitLink(buildCommitUrl(commitBaseUrl, issue.project.vcs, it.hash), it.timestamp, it.hash, it.message)
         })
     }
 
--- a/src/main/resources/localization/strings.properties	Sun Mar 09 17:17:59 2025 +0100
+++ b/src/main/resources/localization/strings.properties	Tue Mar 11 13:59:06 2025 +0100
@@ -85,6 +85,7 @@
 issue.comments=Comments
 issue.commits.hash=Hash
 issue.commits.message=Brief
+issue.commits.timestamp=Timestamp
 issue.commits=Commits
 issue.created=Created
 issue.description=Description
--- a/src/main/resources/localization/strings_de.properties	Sun Mar 09 17:17:59 2025 +0100
+++ b/src/main/resources/localization/strings_de.properties	Tue Mar 11 13:59:06 2025 +0100
@@ -85,6 +85,7 @@
 issue.comments=Kommentare
 issue.commits.hash=Hash
 issue.commits.message=Zusammenfassung
+issue.commits.timestamp=Zeitstempel
 issue.commits=Commits
 issue.created=Erstellt
 issue.description=Beschreibung
--- a/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf	Sun Mar 09 17:17:59 2025 +0100
+++ b/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf	Tue Mar 11 13:59:06 2025 +0100
@@ -35,6 +35,7 @@
         Query Parameter <code>in_project</code> zu globalen Vorgangs-URLs hinzugefügt,
         der von Tools benutzt werden kann, um Vorgänge direkt in der Projektansicht zu öffnen.
     </li>
+    <li>Zeitstempel zu Commit-Referenzen hinzugefügt: dazu muss das Template für die hg log Ausgabe auf <code>;;lpitref;{node};{date|isodatesec};{desc}\n</code> umgestellt werden.</li>
     <li>Die Anzeige von Komponenten in der Vorgangsliste nutzt nun die der Komponente zugewiesene Farbe.</li>
     <li>Das Navigationsmenü bietet nun direkt die Möglichkeit eine neue Komponente/Version/Variante zu erzeugen, falls noch keine existiert.</li>
     <li>Projektdetails werden nun permanent angezeigt.</li>
--- a/src/main/webapp/WEB-INF/changelogs/changelog.jspf	Sun Mar 09 17:17:59 2025 +0100
+++ b/src/main/webapp/WEB-INF/changelogs/changelog.jspf	Tue Mar 11 13:59:06 2025 +0100
@@ -35,6 +35,7 @@
         Add optional query parameter <code>in_project</code> for global issue URLs
         that can be used by tools to directly open an issue in the project view.
     </li>
+    <li>Add commit timestamps to the list of commit references. To support this, change your hg log template to <code>;;lpitref;{node};{date|isodatesec};{desc}\n</code>.</li>
     <li>Change that the component labels in the issue view now use their assigned color.</li>
     <li>Change navigation menu to show a menu item to create a component/version/variant if none exist, yet.</li>
     <li>Change that project details are always shown.</li>
--- a/src/main/webapp/WEB-INF/jsp/issue-view.jsp	Sun Mar 09 17:17:59 2025 +0100
+++ b/src/main/webapp/WEB-INF/jsp/issue-view.jsp	Tue Mar 11 13:59:06 2025 +0100
@@ -188,10 +188,12 @@
     <table class="issue-view fullwidth">
         <colgroup>
             <col>
+            <col>
             <col class="fullwidth">
         </colgroup>
         <thead>
         <tr>
+            <th><fmt:message key="issue.commits.timestamp"/></th>
             <th><fmt:message key="issue.commits.hash"/></th>
             <th><fmt:message key="issue.commits.message"/></th>
         </tr>
@@ -199,6 +201,11 @@
         <tbody>
         <c:forEach var="commitLink" items="${viewmodel.commitLinks}">
         <tr>
+            <td>
+                <c:if test="${not empty commitLink.timestamp}">
+                <fmt:formatDate value="${commitLink.timestamp}" timeZone="${timezone}" type="both"/>
+                </c:if>
+            </td>
             <td><a href="${commitLink.url}" target="_blank">${commitLink.hash}</a></td>
             <td><c:out value="${commitLink.message}"/> </td>
         </tr>
--- a/src/test/kotlin/de/uapcore/lightpit/types/CommitRefTest.kt	Sun Mar 09 17:17:59 2025 +0100
+++ b/src/test/kotlin/de/uapcore/lightpit/types/CommitRefTest.kt	Tue Mar 11 13:59:06 2025 +0100
@@ -26,6 +26,8 @@
 
 package de.uapcore.lightpit.types
 
+import java.sql.Timestamp
+import java.time.Instant
 import kotlin.test.Test
 import kotlin.test.assertContentEquals
 
@@ -38,11 +40,7 @@
                 CommitRef("cf9f5982ddeb28c7f695dc547fe73abf5460016f", 50, "here we fix #50"),
                 CommitRef("cf9f5982ddeb28c7f695dc547fe73abf5460016f", 30, "here we fix #50"),
                 CommitRef("cf9f5982ddeb28c7f695dc547fe73abf5460016f", 80, "here we fix #50"),
-                CommitRef(
-                    "ed7134e5f4ce278c4f62798fb9f96129be2b132b",
-                    1337,
-                    "commit with a #non-ref, relates to #wrong ref but still closes #1337"
-                ),
+                CommitRef("ed7134e5f4ce278c4f62798fb9f96129be2b132b", 1337, "commit with a #non-ref, relates to #wrong ref but still closes #1337"),
                 CommitRef("74d770da3c80c0c3fc1fb7e44fb710d665127dfe", 47, "a change with commitrefs only in body"),
                 CommitRef("74d770da3c80c0c3fc1fb7e44fb710d665127dfe", 13, "a change with commitrefs only in body"),
                 CommitRef("9a14e5628bdf2d578f3385d78022ddcaf23d1abb", 47, "add test file - closed #47 and fixed #90"),
@@ -63,4 +61,33 @@
             )
         )
     }
+
+    @Test
+    fun parseCommitRefsV2() {
+        assertContentEquals(
+            listOf(
+                CommitRef("cf9f5982ddeb28c7f695dc547fe73abf5460016f", 50, "here we fix #50", Timestamp.from(Instant.parse("2025-01-09T11:11:11+11:00"))),
+                CommitRef("cf9f5982ddeb28c7f695dc547fe73abf5460016f", 30, "here we fix #50", Timestamp.from(Instant.parse("2025-01-09T11:11:11+11:00"))),
+                CommitRef("cf9f5982ddeb28c7f695dc547fe73abf5460016f", 80, "here we fix #50", Timestamp.from(Instant.parse("2025-01-09T11:11:11+11:00"))),
+                CommitRef("ed7134e5f4ce278c4f62798fb9f96129be2b132b", 1337, "commit with a #non-ref, relates to #wrong ref but still closes #1337", Timestamp.from(Instant.parse("2024-02-29T12:30:20-03:00"))),
+                CommitRef("74d770da3c80c0c3fc1fb7e44fb710d665127dfe", 47, "a change with commitrefs only in body", Timestamp.from(Instant.parse("2025-03-04T08:15:24+04:00"))),
+                CommitRef("74d770da3c80c0c3fc1fb7e44fb710d665127dfe", 13, "a change with commitrefs only in body", Timestamp.from(Instant.parse("2025-03-04T08:15:24+04:00"))),
+                CommitRef("9a14e5628bdf2d578f3385d78022ddcaf23d1abb", 47, "add test file - closed #47 and fixed #90", Timestamp.from(Instant.parse("2025-03-04T13:37:04+02:00"))),
+                CommitRef("9a14e5628bdf2d578f3385d78022ddcaf23d1abb", 90, "add test file - closed #47 and fixed #90", Timestamp.from(Instant.parse("2025-03-04T13:37:04+02:00")))
+            ).reversed(),
+            parseCommitRefs("""
+;;lpitref;cf9f5982ddeb28c7f695dc547fe73abf5460016f;2025-01-09T11:11:11+11:00;here we fix #50
+
+and resolve #30 which blocked issue #80
+;;lpitref;ed7134e5f4ce278c4f62798fb9f96129be2b132b;2024-02-29T12:30:20-03:00;commit with a #non-ref, relates to #wrong ref but still closes #1337
+;;lpitref;74d770da3c80c0c3fc1fb7e44fb710d665127dfe;2025-03-04T08:15:24+04:00;a change with commitrefs only in body
+
+some more details
+fixes #47 and relates to #13
+;;lpitref;d533c717dfecb8e4b993ca6c8760f1493bc834b6;2025-03-04T14:45:14+02:00;no commitref
+;;lpitref;9a14e5628bdf2d578f3385d78022ddcaf23d1abb;2025-03-04T13:37:04+02:00;add test file - closed #47 and fixed #90
+"""
+            )
+        )
+    }
 }
\ No newline at end of file

mercurial