From c1869ea427836f76ae0961e7e1984080af2a2a8a Mon Sep 17 00:00:00 2001 From: mhoennig Date: Mon, 10 Aug 2026 21:11:15 +0200 Subject: [PATCH] Link index-less report pages from the artifact index (v0.9.8) Gradle's --profile report is archived but was unreachable: report discovery only looked for index.html, while the profile page carries a timestamped file name. Scan reports/ and its direct sub-directories for HTML pages that no index covers, so a report tree cannot flood the index with inner pages. Co-Authored-By: Claude Fable 5 --- build.gradle.kts | 2 +- .../hoennig/gittally/server/UiController.kt | 39 +++++++++++++++++-- src/main/resources/templates/releases.html | 6 +++ .../gittally/server/UiControllerTest.kt | 23 +++++++++++ 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index ba44e86..bb85656 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,7 @@ plugins { group = "de.hoennig" // bump at least the patch version for every deployment, so the UI footer // (BuildProperties) and --version identify what is actually running -version = "0.9.7" +version = "0.9.8" java { toolchain { diff --git a/src/main/kotlin/de/hoennig/gittally/server/UiController.kt b/src/main/kotlin/de/hoennig/gittally/server/UiController.kt index 6acd905..a3f954f 100644 --- a/src/main/kotlin/de/hoennig/gittally/server/UiController.kt +++ b/src/main/kotlin/de/hoennig/gittally/server/UiController.kt @@ -226,7 +226,7 @@ class UiController( } /** - * The browsable `index.html` pages under `reports/`, shallowest first; pages nested + * The browsable report pages under `reports/`, shallowest first; pages nested * below an already-listed report index are skipped — like the legacy artifact index. * Each entry carries the report's failures counter for the failed-badge. */ @@ -248,13 +248,46 @@ class UiController( val topmost = mutableListOf() for (relativeIndex in allIndexes) { val dir = relativeIndex.substringBeforeLast('/', "") - if (knownDirs.any { known -> known.isEmpty() || dir == known || dir.startsWith("$known/") }) { + if (dir.isCoveredBy(knownDirs)) { continue } knownDirs += dir topmost += relativeIndex } - return topmost.map { ReportIndexView(path = it, failures = reportFailures(reportsDir.resolve(it))) } + return (topmost + indexLessReportPages(reportsDir, knownDirs)) + .map { ReportIndexView(path = it, failures = reportFailures(reportsDir.resolve(it))) } + } + + private fun String.isCoveredBy(knownDirs: List): Boolean = + knownDirs.any { known -> known.isEmpty() || this == known || this.startsWith("$known/") } + + /** + * Report pages of directories without an `index.html`, such as Gradle's `--profile` report + * with its timestamped file name. Only `reports/` itself and its direct sub-directories are + * scanned, so that a report tree cannot flood the artifact index with its inner pages. + */ + private fun indexLessReportPages( + reportsDir: Path, + knownDirs: List, + ): List { + val candidateDirs = + buildList { + add(reportsDir) + Files.list(reportsDir).use { children -> + children.asSequence().filter { Files.isDirectory(it) }.forEach { add(it) } + } + } + return candidateDirs + .filterNot { reportsDir.relativize(it).toString().isCoveredBy(knownDirs) } + .flatMap { dir -> + Files.list(dir).use { pages -> + pages + .asSequence() + .filter { Files.isRegularFile(it) && it.name.endsWith(".html") } + .map { reportsDir.relativize(it).toString() } + .toList() + } + }.sorted() } /** diff --git a/src/main/resources/templates/releases.html b/src/main/resources/templates/releases.html index 1d925f5..3107ca0 100644 --- a/src/main/resources/templates/releases.html +++ b/src/main/resources/templates/releases.html @@ -7,6 +7,12 @@
+

v0.9.8 — 2026-08-10

+
    +
  • The artifact index also links report pages of directories without an index.html, + such as Gradle's --profile report with its timestamped file name.
  • +
+

v0.9.7 — 2026-08-10

  • init --systemd also installs a nightly Docker cleanup timer diff --git a/src/test/kotlin/de/hoennig/gittally/server/UiControllerTest.kt b/src/test/kotlin/de/hoennig/gittally/server/UiControllerTest.kt index 54cb9e6..93eb871 100644 --- a/src/test/kotlin/de/hoennig/gittally/server/UiControllerTest.kt +++ b/src/test/kotlin/de/hoennig/gittally/server/UiControllerTest.kt @@ -237,6 +237,29 @@ class UiControllerTest : FunSpec() { ).andExpect(content().string(not(containsString("reports/tests/test/packages/index.html")))) } + test("artifact index links report pages of directories without an index, but not their inner pages") { + val artifactDir = Files.createDirectories(tempDir.resolve("main-abc123-key")) + Files.createDirectories(artifactDir.resolve("reports/profile")) + Files.writeString(artifactDir.resolve("reports/profile/profile-2026-08-10-18-36-12.html"), "") + Files.createDirectories(artifactDir.resolve("reports/tests/test")) + Files.writeString(artifactDir.resolve("reports/tests/test/index.html"), "") + Files.createDirectories(artifactDir.resolve("reports/tests/test/classes")) + Files.writeString(artifactDir.resolve("reports/tests/test/classes/SomeTest.html"), "") + every { repository.history() } returns listOf(successResult) + every { artifactStore.artifactDir("main-abc123-key") } returns artifactDir + + val page = + mockMvc + .perform(get("/builds/main-abc123-key")) + .andExpect(status().isOk) + .andReturn() + .response.contentAsString + + page shouldContain "reports/profile/profile-2026-08-10-18-36-12.html" + page shouldContain "reports/tests/test/index.html" + page shouldNotContain "SomeTest.html" + } + test("report links carry a failed-badge from the report's failures counter") { val artifactDir = Files.createDirectories(tempDir.resolve("main-abc123-key")) Files.createDirectories(artifactDir.resolve("reports/tests/test"))