feat(server): Routen, Seiten und Artefakte tragen das Repository — /repos/<name>/… (Sitzung D)
Zweite Hälfte von Sitzung D (docs/plan/22-multi-repo.md, PR #13): Der Server bediente bisher genau ein Repository der Registry. Jede Route — API, Seiten, Artefakt-Dateien — arbeitete auf `registry.current()`; ein zweites registriertes Repository wurde gebaut und gepollt, war aber unsichtbar und unerreichbar. Jeder Controller löst sein Repository jetzt je Anfrage auf, statt das bediente als Bohne zu halten; jede Route ist zweimal gemappt. Die unscoped Form ist kein Übergangs-Alias, sondern dauerhaft die Art zu sagen „das bediente Repository" — Lesezeichen und die nach Gitea geposteten Links kennen kein Segment. Entschieden gegen die Repo-Spalte: Die Seiten bleiben je Repository, die Navigation bekommt einen Umschalter. Die Aktionen einer Zeile brauchen das Repository ohnehin, Branches kommen von einem origin und Artefakte aus einem Store — und bei dem einen Repository, das die meisten Installationen haben, wäre eine Spalte nur Rauschen. Das Link-Präfix folgt der ZAHL der bedienten Repositories, nicht dem Weg, über den eine Seite erreicht wurde: mit einem behält die Installation ihre bisherigen URLs (Abnahmekriterium der Sitzung), mit mehreren benennt jeder Link sein Repository. werkator.js liest das Präfix einmal aus einem `werkator-repo-base`-Meta. `BranchPermalinks.permanentUrl` bekommt es ebenfalls — der permanente Schlüssel ist ein Hash des Build-Namens allein, zwei Repositories mit `main` teilten sich sonst eine permanente URL. Fünf neue Tests, Gegenprobe per Mutation gezogen (Präfix fest auf leer → genau der Mehr-Repo-Test fällt). 498 Tests grün, ktlint sauber. PR-Dokument docs/prs/2026-09-03-PR#13-…, Plan, Architektur-Skill und AGENTS.md nachgezogen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4304dd7c4b
commit
c82f2a965c
@@ -27,6 +27,7 @@ data class BuildResultDto(
|
||||
fun from(
|
||||
result: BuildResult,
|
||||
isLatestGreen: Boolean = false,
|
||||
base: String = "",
|
||||
) = BuildResultDto(
|
||||
branch = result.branch,
|
||||
name = result.name,
|
||||
@@ -36,7 +37,7 @@ data class BuildResultDto(
|
||||
runningSince = result.runningSince,
|
||||
durationSeconds = result.duration?.seconds,
|
||||
artifactKey = result.artifactKey,
|
||||
latestGreenUrl = if (isLatestGreen) BranchPermalinks.permanentUrl(result.name) else null,
|
||||
latestGreenUrl = if (isLatestGreen) BranchPermalinks.permanentUrl(result.name, base) else null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.hoennig.werkator.server
|
||||
|
||||
import de.hoennig.werkator.build.ArtifactStore
|
||||
import de.hoennig.werkator.repo.RepoContext
|
||||
import de.hoennig.werkator.repo.RepoRegistry
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.springframework.core.io.FileSystemResource
|
||||
import org.springframework.core.io.Resource
|
||||
@@ -25,17 +26,22 @@ import kotlin.streams.asSequence
|
||||
*/
|
||||
@RestController
|
||||
class ArtifactFileController(
|
||||
private val artifactStore: ArtifactStore,
|
||||
private val branchPermalinks: BranchPermalinks,
|
||||
private val registry: RepoRegistry,
|
||||
) {
|
||||
@GetMapping("/artifacts/{artifactKey}/{*path}")
|
||||
/** Scoped and unscoped, like every other route (ADR 0009); unscoped means the served repository. */
|
||||
private fun repoOf(name: String?): RepoContext =
|
||||
if (name == null) registry.current() else registry.byName(name) ?: throw UnknownRepositoryException(name)
|
||||
|
||||
@GetMapping("/artifacts/{artifactKey}/{*path}", "/repos/{repo}/artifacts/{artifactKey}/{*path}")
|
||||
fun serve(
|
||||
@PathVariable(name = "repo", required = false) repoName: String?,
|
||||
@PathVariable artifactKey: String,
|
||||
@PathVariable path: String,
|
||||
request: HttpServletRequest,
|
||||
): ResponseEntity<Resource> {
|
||||
val artifactDir =
|
||||
artifactStore.artifactDir(artifactKey)
|
||||
repoOf(repoName).artifactStore.artifactDir(artifactKey)
|
||||
?: return ResponseEntity.notFound().build()
|
||||
val relativePath = path.removePrefix("/").removeSuffix("/")
|
||||
directoryResponse(artifactDir, relativePath, request, noStore = true)?.let { return it }
|
||||
@@ -52,15 +58,17 @@ class ArtifactFileController(
|
||||
* slash, so relative links inside reports resolve correctly), and everything is
|
||||
* `no-store` because the content behind a URL changes with every new green build.
|
||||
*/
|
||||
@GetMapping("/branches/{branchKey}/{*path}")
|
||||
@GetMapping("/branches/{branchKey}/{*path}", "/repos/{repo}/branches/{branchKey}/{*path}")
|
||||
fun serveLatestGreen(
|
||||
@PathVariable(name = "repo", required = false) repoName: String?,
|
||||
@PathVariable branchKey: String,
|
||||
@PathVariable path: String,
|
||||
request: HttpServletRequest,
|
||||
): ResponseEntity<Resource> {
|
||||
val build = branchPermalinks.latestGreenBuild(branchKey)
|
||||
val repo = repoOf(repoName)
|
||||
val build = branchPermalinks.latestGreenBuild(repo, branchKey)
|
||||
val artifactDir =
|
||||
artifactStore.artifactDir(build.artifactKey)
|
||||
repo.artifactStore.artifactDir(build.artifactKey)
|
||||
?: throw ResponseStatusException(
|
||||
HttpStatus.NOT_FOUND,
|
||||
"the artifacts of build '${build.artifactKey}' are not stored anymore",
|
||||
|
||||
@@ -17,7 +17,10 @@ import org.springframework.stereotype.Component
|
||||
class BranchListing(
|
||||
private val gitService: GitService,
|
||||
) {
|
||||
fun branches(repo: RepoContext): List<BranchDto> {
|
||||
fun branches(
|
||||
repo: RepoContext,
|
||||
base: String = "",
|
||||
): List<BranchDto> {
|
||||
val repository = repo.results
|
||||
val heads = gitService.originBranchHeads(repo.workingDir)
|
||||
val namedResults = repository.latestPerName().filter { it.name != it.branch && it.branch in heads }
|
||||
@@ -45,7 +48,7 @@ class BranchListing(
|
||||
// the permanent link belongs to the build it resolves to, not to every build of the name
|
||||
val isLatestGreen =
|
||||
row.artifactKey.isNotEmpty() && row.artifactKey == repository.latestGreenFor(row.name)?.artifactKey
|
||||
if (isLatestGreen) row.copy(latestGreenUrl = BranchPermalinks.permanentUrl(row.name)) else row
|
||||
if (isLatestGreen) row.copy(latestGreenUrl = BranchPermalinks.permanentUrl(row.name, base)) else row
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.hoennig.werkator.server
|
||||
|
||||
import de.hoennig.werkator.build.ArtifactKeys
|
||||
import de.hoennig.werkator.build.BuildResult
|
||||
import de.hoennig.werkator.build.BuildResultRepository
|
||||
import de.hoennig.werkator.repo.RepoContext
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
@@ -18,10 +18,12 @@ import org.springframework.web.server.ResponseStatusException
|
||||
* artifacts.
|
||||
*/
|
||||
@Component
|
||||
class BranchPermalinks(
|
||||
private val repository: BuildResultRepository,
|
||||
) {
|
||||
fun latestGreenBuild(branchKey: String): BuildResult {
|
||||
class BranchPermalinks {
|
||||
fun latestGreenBuild(
|
||||
repo: RepoContext,
|
||||
branchKey: String,
|
||||
): BuildResult {
|
||||
val repository = repo.results
|
||||
val names =
|
||||
repository
|
||||
.latestPerName()
|
||||
@@ -41,7 +43,16 @@ class BranchPermalinks(
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** The permanent artifact-index URL of the build name (branch or named slot), shown in the branches view. */
|
||||
fun permanentUrl(name: String): String = "/branches/${ArtifactKeys.permanentBranchKey(name)}"
|
||||
/**
|
||||
* The permanent artifact-index URL of the build name (branch or named slot), shown
|
||||
* in the branches view. [base] is the repository prefix (`/repos/<name>`, empty with
|
||||
* one served repository): the key is a hash of the name alone, so two repositories
|
||||
* both having `main` would otherwise share one permanent URL — and it would resolve
|
||||
* against whichever repository the instance happens to serve unscoped.
|
||||
*/
|
||||
fun permanentUrl(
|
||||
name: String,
|
||||
base: String = "",
|
||||
): String = "$base/branches/${ArtifactKeys.permanentBranchKey(name)}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,9 @@ class BuildsApiController(
|
||||
private fun RepoContext.isLatestGreen(result: BuildResult): Boolean =
|
||||
results.latestGreenFor(result.name)?.artifactKey == result.artifactKey
|
||||
|
||||
/** The prefix the permanent links in the answers carry; empty with one served repository. */
|
||||
private fun uiBase(repo: RepoContext): String = if (registry.all().size > 1) "/repos/${repo.name}" else ""
|
||||
|
||||
/** An unknown repository name answers like every other miss of this API: 404 with `error`. */
|
||||
@ExceptionHandler(UnknownRepositoryException::class)
|
||||
fun unknownRepository(e: UnknownRepositoryException): ResponseEntity<Any> = notFound(e.message ?: "unknown repository")
|
||||
@@ -60,21 +63,24 @@ class BuildsApiController(
|
||||
@PathVariable(name = "repo", required = false) repoName: String?,
|
||||
): List<BuildResultDto> {
|
||||
val repo = repoOf(repoName)
|
||||
return repo.results.latestPerName().map { BuildResultDto.from(it, repo.isLatestGreen(it)) }
|
||||
return repo.results.latestPerName().map { BuildResultDto.from(it, repo.isLatestGreen(it), uiBase(repo)) }
|
||||
}
|
||||
|
||||
/** The legacy branches view: every origin branch with its latest build or `unknown`. */
|
||||
@GetMapping("/api/branches", "/api/repos/{repo}/branches")
|
||||
fun branches(
|
||||
@PathVariable(name = "repo", required = false) repoName: String?,
|
||||
): List<BranchDto> = branchListing.branches(repoOf(repoName))
|
||||
): List<BranchDto> {
|
||||
val repo = repoOf(repoName)
|
||||
return branchListing.branches(repo, uiBase(repo))
|
||||
}
|
||||
|
||||
@GetMapping("/api/builds/history", "/api/repos/{repo}/builds/history")
|
||||
fun history(
|
||||
@PathVariable(name = "repo", required = false) repoName: String?,
|
||||
): List<BuildResultDto> {
|
||||
val repo = repoOf(repoName)
|
||||
return repo.results.history().map { BuildResultDto.from(it, repo.isLatestGreen(it)) }
|
||||
return repo.results.history().map { BuildResultDto.from(it, repo.isLatestGreen(it), uiBase(repo)) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
package de.hoennig.werkator.server
|
||||
|
||||
import de.hoennig.werkator.build.ArtifactStore
|
||||
import de.hoennig.werkator.build.BuildExecutor
|
||||
import de.hoennig.werkator.build.BuildResult
|
||||
import de.hoennig.werkator.build.BuildResultRepository
|
||||
import de.hoennig.werkator.build.BuildStatus
|
||||
import de.hoennig.werkator.config.ConfigFiles
|
||||
import de.hoennig.werkator.config.ConfigLoader
|
||||
import de.hoennig.werkator.git.GitService
|
||||
import de.hoennig.werkator.metrics.SystemMetricsCollector
|
||||
import de.hoennig.werkator.repo.RepoContext
|
||||
import de.hoennig.werkator.repo.RepoRegistry
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.boot.info.BuildProperties
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
@@ -34,19 +35,31 @@ import kotlin.streams.asSequence
|
||||
*/
|
||||
@Controller
|
||||
class UiController(
|
||||
private val repository: BuildResultRepository,
|
||||
private val buildExecutor: BuildExecutor,
|
||||
private val artifactStore: ArtifactStore,
|
||||
private val configLoader: ConfigLoader,
|
||||
private val gitService: GitService,
|
||||
private val metricsCollector: SystemMetricsCollector,
|
||||
private val branchListing: BranchListing,
|
||||
private val branchPermalinks: BranchPermalinks,
|
||||
private val buildProperties: ObjectProvider<BuildProperties>,
|
||||
private val repo: RepoContext,
|
||||
private val registry: RepoRegistry,
|
||||
) {
|
||||
private val workingDir: Path
|
||||
get() = repo.workingDir
|
||||
/**
|
||||
* Every page exists twice, like the API (ADR 0009): repository-scoped under
|
||||
* `/repos/<name>/…` and unscoped, which means the served repository. Pages stay
|
||||
* per repository instead of merging every repository's rows into one table with a
|
||||
* repository column: a row's actions (restart, cancel, delete) need the repository
|
||||
* anyway, branches come from one origin and artifacts from one store — and with
|
||||
* the one repository that most installations have, such a column is pure noise.
|
||||
* What makes the instance one UI is the repository switcher in the navigation.
|
||||
*/
|
||||
private fun repoOf(name: String?): RepoContext =
|
||||
if (name == null) registry.current() else registry.byName(name) ?: throw UnknownRepositoryException(name)
|
||||
|
||||
/** An unknown repository name is a 404 page, not a server error. */
|
||||
@ExceptionHandler(UnknownRepositoryException::class)
|
||||
fun unknownRepository(e: UnknownRepositoryException): ResponseEntity<String> =
|
||||
ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.message)
|
||||
|
||||
/**
|
||||
* Permanent redirects for the legacy script's static page names, so bookmarks
|
||||
@@ -58,11 +71,15 @@ class UiController(
|
||||
setStatusCode(HttpStatus.MOVED_PERMANENTLY)
|
||||
}
|
||||
|
||||
@GetMapping("/")
|
||||
fun latest(model: Model): String {
|
||||
val links = baseModel(model, view = "latest", pageTitle = "Latest Builds")
|
||||
model.addAttribute("rows", repository.latestPerName().map { BuildRowView.from(it, links, permanentUrlOf(it)) })
|
||||
model.addAttribute("apiPath", "/api/builds/latest")
|
||||
@GetMapping("/", "/repos/{repo}")
|
||||
fun latest(
|
||||
@PathVariable(name = "repo", required = false) repoName: String?,
|
||||
model: Model,
|
||||
): String {
|
||||
val repo = repoOf(repoName)
|
||||
val links = baseModel(model, view = "latest", pageTitle = "Latest Builds", repo = repo)
|
||||
model.addAttribute("rows", repo.results.latestPerName().map { BuildRowView.from(it, links, permanentUrlOf(repo, it)) })
|
||||
model.addAttribute("apiPath", apiBase(repo) + "/builds/latest")
|
||||
model.addAttribute("allowRestart", true)
|
||||
model.addAttribute("restartAtOriginHead", false)
|
||||
model.addAttribute("emptyMessage", "No builds recorded yet.")
|
||||
@@ -70,11 +87,15 @@ class UiController(
|
||||
}
|
||||
|
||||
/** The legacy branches view: every origin branch with its latest build or an `unknown` row. */
|
||||
@GetMapping("/branches")
|
||||
fun branches(model: Model): String {
|
||||
val links = baseModel(model, view = "branches", pageTitle = "Branches")
|
||||
model.addAttribute("rows", branchListing.branches(repo).map { BuildRowView.from(it, links) })
|
||||
model.addAttribute("apiPath", "/api/branches")
|
||||
@GetMapping("/branches", "/repos/{repo}/branches")
|
||||
fun branches(
|
||||
@PathVariable(name = "repo", required = false) repoName: String?,
|
||||
model: Model,
|
||||
): String {
|
||||
val repo = repoOf(repoName)
|
||||
val links = baseModel(model, view = "branches", pageTitle = "Branches", repo = repo)
|
||||
model.addAttribute("rows", branchListing.branches(repo, uiBase(repo)).map { BuildRowView.from(it, links) })
|
||||
model.addAttribute("apiPath", apiBase(repo) + "/branches")
|
||||
model.addAttribute("allowRestart", true)
|
||||
// a row here stands for a branch, not for a past run
|
||||
model.addAttribute("restartAtOriginHead", true)
|
||||
@@ -82,11 +103,15 @@ class UiController(
|
||||
return "builds"
|
||||
}
|
||||
|
||||
@GetMapping("/history")
|
||||
fun history(model: Model): String {
|
||||
val links = baseModel(model, view = "history", pageTitle = "Build History")
|
||||
model.addAttribute("rows", repository.history().map { BuildRowView.from(it, links, permanentUrlOf(it)) })
|
||||
model.addAttribute("apiPath", "/api/builds/history")
|
||||
@GetMapping("/history", "/repos/{repo}/history")
|
||||
fun history(
|
||||
@PathVariable(name = "repo", required = false) repoName: String?,
|
||||
model: Model,
|
||||
): String {
|
||||
val repo = repoOf(repoName)
|
||||
val links = baseModel(model, view = "history", pageTitle = "Build History", repo = repo)
|
||||
model.addAttribute("rows", repo.results.history().map { BuildRowView.from(it, links, permanentUrlOf(repo, it)) })
|
||||
model.addAttribute("apiPath", apiBase(repo) + "/builds/history")
|
||||
model.addAttribute("allowRestart", false)
|
||||
model.addAttribute("restartAtOriginHead", false)
|
||||
model.addAttribute("emptyMessage", "No builds archived yet.")
|
||||
@@ -94,17 +119,24 @@ class UiController(
|
||||
}
|
||||
|
||||
/** The permanent branch URL belongs to the build it resolves to — the name's latest green build. */
|
||||
private fun permanentUrlOf(result: BuildResult): String? =
|
||||
if (repository.latestGreenFor(result.name)?.artifactKey == result.artifactKey) {
|
||||
BranchPermalinks.permanentUrl(result.name)
|
||||
private fun permanentUrlOf(
|
||||
repo: RepoContext,
|
||||
result: BuildResult,
|
||||
): String? =
|
||||
if (repo.results.latestGreenFor(result.name)?.artifactKey == result.artifactKey) {
|
||||
BranchPermalinks.permanentUrl(result.name, uiBase(repo))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@GetMapping("/current")
|
||||
fun current(model: Model): String {
|
||||
val links = baseModel(model, view = "current", pageTitle = "Current Builds")
|
||||
val results = repository.history()
|
||||
@GetMapping("/current", "/repos/{repo}/current")
|
||||
fun current(
|
||||
@PathVariable(name = "repo", required = false) repoName: String?,
|
||||
model: Model,
|
||||
): String {
|
||||
val repo = repoOf(repoName)
|
||||
val links = baseModel(model, view = "current", pageTitle = "Current Builds", repo = repo)
|
||||
val results = repo.results.history()
|
||||
val currentBuilds =
|
||||
buildExecutor.currentBuilds().filter { it.repo === repo }.map { build ->
|
||||
CurrentBuildView(
|
||||
@@ -130,35 +162,39 @@ class UiController(
|
||||
/** Hand-maintained release notes (templates/releases.html); linked from the version in the footer. */
|
||||
@GetMapping("/releases")
|
||||
fun releases(model: Model): String {
|
||||
baseModel(model, view = "releases", pageTitle = "Release Notes")
|
||||
baseModel(model, view = "releases", pageTitle = "Release Notes", repo = registry.current())
|
||||
return "releases"
|
||||
}
|
||||
|
||||
/** One metrics page for the whole instance — the resources are the instance's, not a repository's. */
|
||||
@GetMapping("/system")
|
||||
fun system(model: Model): String {
|
||||
baseModel(model, view = "system", pageTitle = "System Metrics")
|
||||
baseModel(model, view = "system", pageTitle = "System Metrics", repo = registry.current())
|
||||
model.addAttribute("metrics", SystemMetricsView.from(metricsCollector.snapshot()))
|
||||
return "system"
|
||||
}
|
||||
|
||||
/** Artifact index rendered from the artifact store — legacy pre-generated this page as static HTML. */
|
||||
@GetMapping("/builds/{artifactKey}")
|
||||
@GetMapping("/builds/{artifactKey}", "/repos/{repo}/builds/{artifactKey}")
|
||||
fun artifactIndex(
|
||||
@PathVariable(name = "repo", required = false) repoName: String?,
|
||||
@PathVariable artifactKey: String,
|
||||
model: Model,
|
||||
): String {
|
||||
val result = repository.history().firstOrNull { it.artifactKey == artifactKey }
|
||||
val artifactDir = artifactStore.artifactDir(artifactKey)
|
||||
val repo = repoOf(repoName)
|
||||
val result = repo.results.history().firstOrNull { it.artifactKey == artifactKey }
|
||||
val artifactDir = repo.artifactStore.artifactDir(artifactKey)
|
||||
if (result == null && artifactDir == null) {
|
||||
throw ResponseStatusException(HttpStatus.NOT_FOUND, "no build with artifact key '$artifactKey'")
|
||||
}
|
||||
return artifactIndexView(
|
||||
model,
|
||||
pageTitle = "Build Artifacts",
|
||||
repo = repo,
|
||||
result = result,
|
||||
artifactKey = artifactKey,
|
||||
artifactDir = artifactDir,
|
||||
filesBase = "/artifacts/$artifactKey",
|
||||
filesBase = uiBase(repo) + "/artifacts/$artifactKey",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -167,38 +203,42 @@ class UiController(
|
||||
* stay on the permanent `/branches/…` paths, so every link copied from this page
|
||||
* outlives artifact pruning.
|
||||
*/
|
||||
@GetMapping("/branches/{branchKey}")
|
||||
@GetMapping("/branches/{branchKey}", "/repos/{repo}/branches/{branchKey}")
|
||||
fun latestGreenArtifactIndex(
|
||||
@PathVariable(name = "repo", required = false) repoName: String?,
|
||||
@PathVariable branchKey: String,
|
||||
model: Model,
|
||||
): String {
|
||||
val build = branchPermalinks.latestGreenBuild(branchKey)
|
||||
val repo = repoOf(repoName)
|
||||
val build = branchPermalinks.latestGreenBuild(repo, branchKey)
|
||||
model.addAttribute("permanentBranch", build.branch)
|
||||
model.addAttribute("concreteUrl", "/builds/${build.artifactKey}")
|
||||
model.addAttribute("concreteUrl", uiBase(repo) + "/builds/${build.artifactKey}")
|
||||
return artifactIndexView(
|
||||
model,
|
||||
pageTitle = "Latest Green Build",
|
||||
repo = repo,
|
||||
result = build,
|
||||
artifactKey = build.artifactKey,
|
||||
artifactDir = artifactStore.artifactDir(build.artifactKey),
|
||||
filesBase = "/branches/$branchKey",
|
||||
artifactDir = repo.artifactStore.artifactDir(build.artifactKey),
|
||||
filesBase = uiBase(repo) + "/branches/$branchKey",
|
||||
)
|
||||
}
|
||||
|
||||
private fun artifactIndexView(
|
||||
model: Model,
|
||||
pageTitle: String,
|
||||
repo: RepoContext,
|
||||
result: BuildResult?,
|
||||
artifactKey: String,
|
||||
artifactDir: Path?,
|
||||
filesBase: String,
|
||||
): String {
|
||||
val links = baseModel(model, view = "artifact", pageTitle = pageTitle)
|
||||
val links = baseModel(model, view = "artifact", pageTitle = pageTitle, repo = repo)
|
||||
model.addAttribute("artifactKey", artifactKey)
|
||||
model.addAttribute("filesBase", filesBase)
|
||||
model.addAttribute("result", result?.let { BuildRowView.from(it, links) })
|
||||
model.addAttribute("hasArtifacts", artifactDir != null)
|
||||
model.addAttribute("buildCommand", result?.let { buildCommandOf(it) })
|
||||
model.addAttribute("buildCommand", result?.let { buildCommandOf(repo, it) })
|
||||
model.addAttribute(
|
||||
"logs",
|
||||
artifactDir?.let { logFiles(it, scanForFailure = result != null && result.status != BuildStatus.SUCCESS) }
|
||||
@@ -226,21 +266,41 @@ class UiController(
|
||||
.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* The prefix every in-page link and API path is built from. It follows the number
|
||||
* of served repositories, not the route the page was reached through: with one
|
||||
* repository the installation keeps its existing URLs (the session-D acceptance
|
||||
* criterion), with several every link names its repository.
|
||||
*/
|
||||
private fun uiBase(repo: RepoContext): String = if (registry.all().size > 1) "/repos/${repo.name}" else ""
|
||||
|
||||
private fun apiBase(repo: RepoContext): String = if (registry.all().size > 1) "/api/repos/${repo.name}" else "/api"
|
||||
|
||||
/** Adds the attributes every page needs and returns the Gitea link helper for row building. */
|
||||
private fun baseModel(
|
||||
model: Model,
|
||||
view: String,
|
||||
pageTitle: String,
|
||||
repo: RepoContext,
|
||||
): GiteaWebLinks {
|
||||
val config = configLoader.load(workingDir)
|
||||
val config = configLoader.load(repo.workingDir)
|
||||
val links = GiteaWebLinks(config.gitea)
|
||||
val repoName =
|
||||
listOf(config.gitea.owner.trim(), config.gitea.repo.trim())
|
||||
.filter { it.isNotEmpty() }
|
||||
.joinToString("/")
|
||||
val served = registry.all()
|
||||
model.addAttribute("view", view)
|
||||
model.addAttribute("pageTitle", pageTitle)
|
||||
model.addAttribute("repoName", repoName)
|
||||
// every in-page link is built from this prefix, so a scoped page stays scoped
|
||||
model.addAttribute("repoBase", uiBase(repo))
|
||||
model.addAttribute("homeUrl", uiBase(repo).ifEmpty { "/" })
|
||||
model.addAttribute("apiBase", apiBase(repo))
|
||||
model.addAttribute("repoKey", repo.name)
|
||||
// the switcher is what makes several repositories one UI; with one there is nothing to switch
|
||||
model.addAttribute("multiRepo", served.size > 1)
|
||||
model.addAttribute("repos", served.map { RepoLinkView(name = it.name, url = "/repos/${it.name}", current = it === repo) })
|
||||
model.addAttribute("version", buildProperties.getIfAvailable()?.version ?: "dev")
|
||||
model.addAttribute("impressumUrl", config.server.impressumUrl.trim())
|
||||
model.addAttribute("giteaRepoUrl", links.repoUrl ?: "")
|
||||
@@ -254,7 +314,11 @@ class UiController(
|
||||
* build of this pool ever ran — the branch and its job usually override it.
|
||||
* The command used by a past run is not persisted, so this is the current answer.
|
||||
*/
|
||||
private fun buildCommandOf(result: BuildResult): String {
|
||||
private fun buildCommandOf(
|
||||
repo: RepoContext,
|
||||
result: BuildResult,
|
||||
): String {
|
||||
val workingDir = repo.workingDir
|
||||
val config =
|
||||
try {
|
||||
configLoader.loadWithBranchLayer(
|
||||
|
||||
@@ -252,3 +252,10 @@ data class SystemMetricsView(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** One entry of the repository switcher in the navigation (ADR 0009). */
|
||||
data class RepoLinkView(
|
||||
val name: String,
|
||||
val url: String,
|
||||
val current: Boolean,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user