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:
@@ -0,0 +1,64 @@
|
||||
package de.hoennig.gittally.server
|
||||
|
||||
import de.hoennig.gittally.build.BuildResult
|
||||
import de.hoennig.gittally.build.BuildResultRepository
|
||||
import de.hoennig.gittally.build.BuildStatus
|
||||
import de.hoennig.gittally.git.GitService
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
class BranchListingTest : FunSpec() {
|
||||
private val gitService = mockk<GitService>()
|
||||
private val repository = mockk<BuildResultRepository>()
|
||||
private val listing = BranchListing(gitService, repository)
|
||||
|
||||
private val mainResult =
|
||||
BuildResult(
|
||||
branch = "main",
|
||||
commit = "0123456789abcdef0123456789abcdef01234567",
|
||||
status = BuildStatus.SUCCESS,
|
||||
startedAt = Instant.parse("2026-07-07T10:00:00Z"),
|
||||
duration = Duration.ofSeconds(83),
|
||||
artifactKey = "main-abc123-key",
|
||||
)
|
||||
|
||||
init {
|
||||
test("orders main/master first, then flat names, then hierarchical names") {
|
||||
every { gitService.originBranchHeads(any()) } returns
|
||||
mapOf(
|
||||
"feature/x" to "aaa",
|
||||
"zz-flat" to "bbb",
|
||||
"main" to "ccc",
|
||||
"aa/nested" to "ddd",
|
||||
"develop" to "eee",
|
||||
)
|
||||
every { repository.latestFor(any()) } returns null
|
||||
|
||||
listing.branches().map { it.branch } shouldBe
|
||||
listOf("main", "develop", "zz-flat", "aa/nested", "feature/x")
|
||||
}
|
||||
|
||||
test("joins the latest build and marks never-built branches as unknown with the origin head") {
|
||||
every { gitService.originBranchHeads(any()) } returns
|
||||
mapOf("main" to "newer-head", "feature/x" to "fedcba98")
|
||||
every { repository.latestFor("main") } returns mainResult
|
||||
every { repository.latestFor("feature/x") } returns null
|
||||
|
||||
val branches = listing.branches()
|
||||
|
||||
branches[0].branch shouldBe "main"
|
||||
branches[0].status shouldBe "success"
|
||||
branches[0].commit shouldBe mainResult.commit
|
||||
branches[0].artifactKey shouldBe "main-abc123-key"
|
||||
branches[1].branch shouldBe "feature/x"
|
||||
branches[1].status shouldBe "unknown"
|
||||
branches[1].commit shouldBe "fedcba98"
|
||||
branches[1].startedAt shouldBe null
|
||||
branches[1].artifactKey shouldBe ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import de.hoennig.gittally.build.BuildResult
|
||||
import de.hoennig.gittally.build.BuildResultRepository
|
||||
import de.hoennig.gittally.build.BuildStatus
|
||||
import de.hoennig.gittally.build.RunningBuild
|
||||
import de.hoennig.gittally.git.GitService
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
@@ -43,6 +44,12 @@ class BuildsApiControllerTest : FunSpec() {
|
||||
@MockkBean
|
||||
lateinit var controlTokens: ControlTokenService
|
||||
|
||||
@MockkBean
|
||||
lateinit var gitService: GitService
|
||||
|
||||
@MockkBean
|
||||
lateinit var branchListing: BranchListing
|
||||
|
||||
private val startedAt = Instant.parse("2026-07-07T10:00:00Z")
|
||||
|
||||
private val successResult =
|
||||
@@ -67,7 +74,7 @@ class BuildsApiControllerTest : FunSpec() {
|
||||
|
||||
init {
|
||||
beforeEach {
|
||||
clearMocks(repository, buildExecutor, artifactStore, controlTokens)
|
||||
clearMocks(repository, buildExecutor, artifactStore, controlTokens, gitService, branchListing)
|
||||
every { controlTokens.matches(any()) } answers { firstArg<String?>() == "secret" }
|
||||
}
|
||||
|
||||
@@ -147,14 +154,50 @@ class BuildsApiControllerTest : FunSpec() {
|
||||
verify { buildExecutor.startBuild("feature/topic", successResult.commit) }
|
||||
}
|
||||
|
||||
test("restart of a branch without recorded builds answers 404") {
|
||||
test("restart of a never-built branch enqueues its origin head commit") {
|
||||
val liveLogFile = tempDir.resolve("first-build.log")
|
||||
every { repository.latestFor("fresh") } returns null
|
||||
every { gitService.originHeadCommit("fresh", any()) } returns successResult.commit
|
||||
every { buildExecutor.startBuild("fresh", successResult.commit) } returns
|
||||
runningBuild(liveLogFile).copy(branch = "fresh")
|
||||
|
||||
mockMvc
|
||||
.perform(post("/api/builds/restart").param("branch", "fresh").param("token", "secret"))
|
||||
.andExpect(status().isAccepted)
|
||||
.andExpect(jsonPath("$.status").value("pending"))
|
||||
|
||||
verify { buildExecutor.startBuild("fresh", successResult.commit) }
|
||||
}
|
||||
|
||||
test("restart of a branch without recorded builds and without origin counterpart answers 404") {
|
||||
every { repository.latestFor("gone") } returns null
|
||||
every { gitService.originHeadCommit("gone", any()) } returns null
|
||||
|
||||
mockMvc
|
||||
.perform(post("/api/builds/restart").param("branch", "gone").param("token", "secret"))
|
||||
.andExpect(status().isNotFound)
|
||||
}
|
||||
|
||||
test("branches answers the branch listing with unknown placeholders for never-built branches") {
|
||||
every { branchListing.branches(any()) } returns
|
||||
listOf(
|
||||
BranchDto.from("main", "ignored-head", successResult),
|
||||
BranchDto.from("feature/x", "fedcba9876543210fedcba9876543210fedcba98", null),
|
||||
)
|
||||
|
||||
mockMvc
|
||||
.perform(get("/api/branches"))
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$[0].branch").value("main"))
|
||||
.andExpect(jsonPath("$[0].status").value("success"))
|
||||
.andExpect(jsonPath("$[0].commit").value(successResult.commit))
|
||||
.andExpect(jsonPath("$[1].branch").value("feature/x"))
|
||||
.andExpect(jsonPath("$[1].status").value("unknown"))
|
||||
.andExpect(jsonPath("$[1].commit").value("fedcba9876543210fedcba9876543210fedcba98"))
|
||||
.andExpect(jsonPath("$[1].startedAt").doesNotExist())
|
||||
.andExpect(jsonPath("$[1].artifactKey").value(""))
|
||||
}
|
||||
|
||||
test("restart with a wrong token answers 403 and does not build") {
|
||||
mockMvc
|
||||
.perform(
|
||||
|
||||
@@ -55,6 +55,9 @@ class UiControllerTest : FunSpec() {
|
||||
@MockkBean
|
||||
lateinit var metricsCollector: SystemMetricsCollector
|
||||
|
||||
@MockkBean
|
||||
lateinit var branchListing: BranchListing
|
||||
|
||||
private val startedAt = Instant.parse("2026-07-07T10:00:00Z")
|
||||
|
||||
private val emptySystemMetrics =
|
||||
@@ -85,7 +88,7 @@ class UiControllerTest : FunSpec() {
|
||||
|
||||
init {
|
||||
beforeEach {
|
||||
clearMocks(repository, buildExecutor, artifactStore, controlTokens, configLoader, metricsCollector)
|
||||
clearMocks(repository, buildExecutor, artifactStore, controlTokens, configLoader, metricsCollector, branchListing)
|
||||
every { configLoader.load(any()) } returns
|
||||
GitTallyConfig(
|
||||
server = ServerConfig(impressumUrl = "https://example.org/imprint"),
|
||||
@@ -102,6 +105,7 @@ class UiControllerTest : FunSpec() {
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(content().string(containsString("No builds recorded yet.")))
|
||||
.andExpect(content().string(containsString("""data-api="/api/builds/latest"""")))
|
||||
.andExpect(content().string(containsString("""id="reload-button"""")))
|
||||
}
|
||||
|
||||
test("latest view renders rows with badge, Gitea links, artifact link, actions, and token") {
|
||||
@@ -122,6 +126,24 @@ class UiControllerTest : FunSpec() {
|
||||
.andExpect(content().string(containsString("1:23")))
|
||||
}
|
||||
|
||||
test("branches view renders built and never-built branches with restart actions") {
|
||||
every { branchListing.branches(any()) } returns
|
||||
listOf(
|
||||
BranchDto.from("main", "ignored-head", successResult),
|
||||
BranchDto.from("feature/x", "fedcba9876543210fedcba9876543210fedcba98", null),
|
||||
)
|
||||
|
||||
mockMvc
|
||||
.perform(get("/branches"))
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(content().string(containsString("status status-success")))
|
||||
.andExpect(content().string(containsString("status status-unknown")))
|
||||
.andExpect(content().string(containsString("feature/x")))
|
||||
.andExpect(content().string(containsString("fedcba987654")))
|
||||
.andExpect(content().string(containsString("""data-api="/api/branches"""")))
|
||||
.andExpect(content().string(containsString("""data-action="restart"""")))
|
||||
}
|
||||
|
||||
test("history view renders mixed history without restart actions") {
|
||||
every { repository.history() } returns
|
||||
listOf(
|
||||
|
||||
Reference in New Issue
Block a user