Tue, 11 Mar 2025 13:59:06 +0100
add timestamp to commit references
resolves #539
--- 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