reintroduced legacy Branches view: added /branches endpoint for listing origin branches and their latest builds (or unknown for never-built branches), updated UI with reload button and navigation, and enhanced API and tests
This commit is contained in:
@@ -52,6 +52,17 @@ class GitService(
|
||||
.lines()
|
||||
.filter { it != "HEAD" }
|
||||
|
||||
/** All origin branches with their head commit, in one git call; refnames cannot contain spaces. */
|
||||
fun originBranchHeads(workingDir: Path = Paths.get(".")): Map<String, String> =
|
||||
runner
|
||||
.runOrThrow(
|
||||
listOf("git", "for-each-ref", "--format=%(refname:strip=3) %(objectname)", "refs/remotes/origin"),
|
||||
workingDir,
|
||||
).lines()
|
||||
.map { it.substringBeforeLast(' ') to it.substringAfterLast(' ') }
|
||||
.filter { (branch, _) -> branch != "HEAD" }
|
||||
.toMap()
|
||||
|
||||
/**
|
||||
* A branch has new commits when its origin counterpart is ahead of the local branch,
|
||||
* or when it exists only on origin.
|
||||
|
||||
@@ -29,6 +29,45 @@ data class BuildResultDto(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One entry of `GET /api/branches`, like a legacy branches-view row: an origin
|
||||
* branch with its latest build, or an `unknown` placeholder when never built.
|
||||
*/
|
||||
data class BranchDto(
|
||||
val branch: String,
|
||||
val commit: String,
|
||||
val status: String,
|
||||
val startedAt: Instant?,
|
||||
val durationSeconds: Long?,
|
||||
val artifactKey: String,
|
||||
) {
|
||||
companion object {
|
||||
fun from(
|
||||
branch: String,
|
||||
headCommit: String,
|
||||
latest: BuildResult?,
|
||||
) = if (latest == null) {
|
||||
BranchDto(
|
||||
branch = branch,
|
||||
commit = headCommit,
|
||||
status = CommitStatusDto.UNKNOWN_STATUS,
|
||||
startedAt = null,
|
||||
durationSeconds = null,
|
||||
artifactKey = "",
|
||||
)
|
||||
} else {
|
||||
BranchDto(
|
||||
branch = branch,
|
||||
commit = latest.commit,
|
||||
status = latest.status.jsonName,
|
||||
startedAt = latest.startedAt,
|
||||
durationSeconds = latest.duration?.seconds,
|
||||
artifactKey = latest.artifactKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** One entry of `GET /api/builds/current`; the log grows while the build runs. */
|
||||
data class CurrentBuildDto(
|
||||
val branch: String,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.hoennig.gittally.server
|
||||
|
||||
import de.hoennig.gittally.build.BuildResultRepository
|
||||
import de.hoennig.gittally.git.GitService
|
||||
import org.springframework.stereotype.Component
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
/**
|
||||
* The branches-view data, shared by the JSON API and the server-rendered page:
|
||||
* every origin branch joined with its latest build (or an `unknown` placeholder
|
||||
* when never built), ordered like the legacy branches view — main/master first,
|
||||
* then flat names, then hierarchical names, alphabetical within each group.
|
||||
* Legacy listed local branches; the new watcher's branch universe is origin.
|
||||
*/
|
||||
@Component
|
||||
class BranchListing(
|
||||
private val gitService: GitService,
|
||||
private val repository: BuildResultRepository,
|
||||
) {
|
||||
fun branches(workingDir: Path = Paths.get(".")): List<BranchDto> =
|
||||
gitService
|
||||
.originBranchHeads(workingDir)
|
||||
.entries
|
||||
.sortedWith(compareBy({ sortGroup(it.key) }, { it.key }))
|
||||
.map { (branch, headCommit) -> BranchDto.from(branch, headCommit, repository.latestFor(branch)) }
|
||||
|
||||
private fun sortGroup(branch: String): Int =
|
||||
when {
|
||||
branch == "main" || branch == "master" -> 0
|
||||
'/' !in branch -> 1
|
||||
else -> 2
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import de.hoennig.gittally.build.ArtifactStore
|
||||
import de.hoennig.gittally.build.BuildExecutor
|
||||
import de.hoennig.gittally.build.BuildResultRepository
|
||||
import de.hoennig.gittally.build.BuildStatus
|
||||
import de.hoennig.gittally.git.GitService
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
@@ -17,6 +18,7 @@ import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardOpenOption
|
||||
|
||||
/**
|
||||
@@ -30,10 +32,18 @@ class BuildsApiController(
|
||||
private val buildExecutor: BuildExecutor,
|
||||
private val artifactStore: ArtifactStore,
|
||||
private val controlTokens: ControlTokenService,
|
||||
private val gitService: GitService,
|
||||
private val branchListing: BranchListing,
|
||||
) {
|
||||
var workingDir: Path = Paths.get(".")
|
||||
|
||||
@GetMapping("/api/builds/latest")
|
||||
fun latest(): List<BuildResultDto> = repository.latestPerBranch().map { BuildResultDto.from(it) }
|
||||
|
||||
/** The legacy branches view: every origin branch with its latest build or `unknown`. */
|
||||
@GetMapping("/api/branches")
|
||||
fun branches(): List<BranchDto> = branchListing.branches(workingDir)
|
||||
|
||||
@GetMapping("/api/builds/history")
|
||||
fun history(): List<BuildResultDto> = repository.history().map { BuildResultDto.from(it) }
|
||||
|
||||
@@ -68,7 +78,8 @@ class BuildsApiController(
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-enqueues the branch's last recorded commit, like the legacy `/control/restart`.
|
||||
* Re-enqueues the branch's last recorded commit — or its origin head for a branch
|
||||
* never built, so the branches view can trigger first builds like legacy.
|
||||
* The branch is a parameter, not a path variable, because branch names may contain
|
||||
* slashes (Tomcat rejects encoded slashes in the path by default).
|
||||
*/
|
||||
@@ -79,10 +90,11 @@ class BuildsApiController(
|
||||
@RequestParam(name = "token", required = false) paramToken: String?,
|
||||
): ResponseEntity<Any> {
|
||||
rejectBadToken(headerToken ?: paramToken)?.let { return it }
|
||||
val latest =
|
||||
repository.latestFor(branch)
|
||||
?: return notFound("branch '$branch' has no recorded build")
|
||||
val running = buildExecutor.startBuild(branch, latest.commit)
|
||||
val commit =
|
||||
repository.latestFor(branch)?.commit
|
||||
?: gitService.originHeadCommit(branch, workingDir)
|
||||
?: return notFound("branch '$branch' has no recorded build and no origin counterpart")
|
||||
val running = buildExecutor.startBuild(branch, commit)
|
||||
return ResponseEntity.accepted().body(
|
||||
BuildResultDto(
|
||||
branch = running.branch,
|
||||
|
||||
@@ -34,6 +34,7 @@ class UiController(
|
||||
private val controlTokens: ControlTokenService,
|
||||
private val configLoader: ConfigLoader,
|
||||
private val metricsCollector: SystemMetricsCollector,
|
||||
private val branchListing: BranchListing,
|
||||
private val buildProperties: ObjectProvider<BuildProperties>,
|
||||
) {
|
||||
var workingDir: Path = Paths.get(".")
|
||||
@@ -48,6 +49,17 @@ class UiController(
|
||||
return "builds"
|
||||
}
|
||||
|
||||
/** 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(workingDir).map { BuildRowView.from(it, links) })
|
||||
model.addAttribute("apiPath", "/api/branches")
|
||||
model.addAttribute("allowRestart", true)
|
||||
model.addAttribute("emptyMessage", "No branches found on origin.")
|
||||
return "builds"
|
||||
}
|
||||
|
||||
@GetMapping("/history")
|
||||
fun history(model: Model): String {
|
||||
val links = baseModel(model, view = "history", pageTitle = "Build History")
|
||||
|
||||
@@ -96,6 +96,23 @@ data class BuildRowView(
|
||||
branchUrl = links.branchUrl(result.branch),
|
||||
commitUrl = links.commitUrl(result.commit),
|
||||
)
|
||||
|
||||
/** A branches-view row; never-built branches have no timestamps and no artifact. */
|
||||
fun from(
|
||||
entry: BranchDto,
|
||||
links: GiteaWebLinks,
|
||||
) = BuildRowView(
|
||||
branch = entry.branch,
|
||||
commit = entry.commit,
|
||||
commitAbbrev = entry.commit.take(12),
|
||||
status = entry.status,
|
||||
startedAtIso = entry.startedAt?.toString() ?: "",
|
||||
startedAt = entry.startedAt?.let { UiFormats.timestamp(it) } ?: "",
|
||||
duration = UiFormats.duration(entry.durationSeconds?.let { Duration.ofSeconds(it) }),
|
||||
artifactKey = entry.artifactKey,
|
||||
branchUrl = links.branchUrl(entry.branch),
|
||||
commitUrl = links.commitUrl(entry.commit),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user