Step 22 C: --repo selects a registered repository in build, retry, and status

The RepoOption mixin resolves the name through the registry; without it a
command means the current directory when served, else the first entry. An
unknown name is a usage error naming the registered repositories.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-09-02 09:03:07 +02:00
co-authored by Claude Fable 5.1
parent a5027acfaf
commit b66d26a03a
7 changed files with 109 additions and 12 deletions
@@ -3,9 +3,11 @@ package de.hoennig.werkator.commands
import de.hoennig.werkator.build.BuildStatus
import de.hoennig.werkator.git.GitService
import de.hoennig.werkator.repo.RepoContext
import de.hoennig.werkator.repo.RepoRegistry
import org.springframework.stereotype.Component
import picocli.CommandLine.Command
import picocli.CommandLine.ExitCode
import picocli.CommandLine.Mixin
import picocli.CommandLine.Parameters
import java.nio.file.Path
import java.util.concurrent.Callable
@@ -24,9 +26,11 @@ import java.util.concurrent.Callable
class BuildCommand(
private val gitService: GitService,
private val consoleBuildRunner: ConsoleBuildRunner,
/** The repository to build: the current working directory (a repo selector comes with the registry). */
var repo: RepoContext,
private val registry: RepoRegistry,
) : Callable<Int> {
@Mixin
var repoOption = RepoOption()
@Parameters(
index = "0",
arity = "0..1",
@@ -35,6 +39,8 @@ class BuildCommand(
)
var branchFragment: String? = null
private lateinit var repo: RepoContext
private val workingDir: Path
get() = repo.workingDir
@@ -42,6 +48,7 @@ class BuildCommand(
val branch: String
val commit: String
try {
repo = repoOption.select(registry)
fetchBestEffort()
branch = resolveBranch() ?: return ExitCode.USAGE
commit = commitToBuild(branch) ?: return ExitCode.USAGE
@@ -0,0 +1,28 @@
package de.hoennig.werkator.commands
import de.hoennig.werkator.repo.RepoContext
import de.hoennig.werkator.repo.RepoRegistry
import picocli.CommandLine.Option
/**
* The `--repo` selector of the repository-scoped commands (ADR 0009): names an entry
* of the instance registry. Without it a command means the current working directory
* when that is served, otherwise the first registered repository — so inside a
* repository every command behaves exactly as it did with one.
*/
class RepoOption {
@Option(
names = ["--repo"],
paramLabel = "<name>",
description = ["registered repository to act on (default: the current directory)"],
)
var name: String? = null
fun select(registry: RepoRegistry): RepoContext {
val wanted = name?.trim()?.takeIf { it.isNotEmpty() } ?: return registry.current()
return registry.byName(wanted)
?: throw IllegalArgumentException(
"no repository named '$wanted' is registered (registered: ${registry.all().joinToString(", ") { it.name }})",
)
}
}
@@ -4,9 +4,11 @@ import de.hoennig.werkator.build.BuildResult
import de.hoennig.werkator.build.BuildStatus
import de.hoennig.werkator.git.GitService
import de.hoennig.werkator.repo.RepoContext
import de.hoennig.werkator.repo.RepoRegistry
import org.springframework.stereotype.Component
import picocli.CommandLine.Command
import picocli.CommandLine.ExitCode
import picocli.CommandLine.Mixin
import java.nio.file.Path
import java.util.concurrent.Callable
@@ -25,15 +27,20 @@ import java.util.concurrent.Callable
class RetryCommand(
private val gitService: GitService,
private val consoleBuildRunner: ConsoleBuildRunner,
/** The repository to retry in: the current working directory (a repo selector comes with the registry). */
var repo: RepoContext,
private val registry: RepoRegistry,
) : Callable<Int> {
@Mixin
var repoOption = RepoOption()
private lateinit var repo: RepoContext
private val workingDir: Path
get() = repo.workingDir
override fun call(): Int {
val failed: List<BuildResult>
try {
repo = repoOption.select(registry)
fetchBestEffort()
failed = repo.results.latestPerName().filter { it.status == BuildStatus.FAILED }
} catch (e: Exception) {
@@ -1,11 +1,12 @@
package de.hoennig.werkator.commands
import de.hoennig.werkator.build.BuildResult
import de.hoennig.werkator.build.BuildResultRepository
import de.hoennig.werkator.repo.RepoRegistry
import de.hoennig.werkator.server.UiFormats
import org.springframework.stereotype.Component
import picocli.CommandLine.Command
import picocli.CommandLine.ExitCode
import picocli.CommandLine.Mixin
import picocli.CommandLine.Option
import java.util.concurrent.Callable
@@ -20,12 +21,22 @@ import java.util.concurrent.Callable
mixinStandardHelpOptions = true,
)
class StatusCommand(
private val repository: BuildResultRepository,
private val registry: RepoRegistry,
) : Callable<Int> {
@Option(names = ["--history"], description = ["Print all recorded builds, not only the latest per branch"])
var history: Boolean = false
@Mixin
var repoOption = RepoOption()
override fun call(): Int {
val repository =
try {
repoOption.select(registry).results
} catch (e: IllegalArgumentException) {
System.err.println("error: ${e.message}")
return ExitCode.USAGE
}
val results = if (history) repository.history() else repository.latestPerName()
if (results.isEmpty()) {
println("(no builds recorded)")
@@ -3,6 +3,7 @@ package de.hoennig.werkator.commands
import de.hoennig.werkator.build.BuildStatus
import de.hoennig.werkator.git.GitService
import de.hoennig.werkator.repo.RepoContext
import de.hoennig.werkator.repo.RepoRegistry
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
@@ -20,9 +21,10 @@ class BuildCommandTest : FunSpec() {
private val consoleBuildRunner = mockk<ConsoleBuildRunner>()
private val dir: Path = Paths.get(".")
private val repo = RepoContext("test", dir, mockk(), mockk())
private val registry = mockk<RepoRegistry>().also { every { it.current() } returns repo }
private fun command(fragment: String? = null) =
BuildCommand(gitService, consoleBuildRunner, repo).apply {
BuildCommand(gitService, consoleBuildRunner, registry).apply {
branchFragment = fragment
}
@@ -5,6 +5,7 @@ import de.hoennig.werkator.build.BuildResultRepository
import de.hoennig.werkator.build.BuildStatus
import de.hoennig.werkator.git.GitService
import de.hoennig.werkator.repo.RepoContext
import de.hoennig.werkator.repo.RepoRegistry
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
@@ -24,8 +25,9 @@ class RetryCommandTest : FunSpec() {
private val consoleBuildRunner = mockk<ConsoleBuildRunner>()
private val dir: Path = Paths.get(".")
private val repo = RepoContext("test", dir, repository, mockk())
private val registry = mockk<RepoRegistry>().also { every { it.current() } returns repo }
private fun command() = RetryCommand(gitService, consoleBuildRunner, repo)
private fun command() = RetryCommand(gitService, consoleBuildRunner, registry)
private fun result(
branch: String,
@@ -3,6 +3,8 @@ package de.hoennig.werkator.commands
import de.hoennig.werkator.build.BuildResult
import de.hoennig.werkator.build.BuildResultRepository
import de.hoennig.werkator.build.BuildStatus
import de.hoennig.werkator.repo.RepoContext
import de.hoennig.werkator.repo.RepoRegistry
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
@@ -16,6 +18,30 @@ import java.time.Instant
class StatusCommandTest : FunSpec() {
private val repository = mockk<BuildResultRepository>()
private val other = mockk<BuildResultRepository>()
private val registry =
mockk<RepoRegistry>().also {
val current =
RepoContext(
"current",
java.nio.file.Paths
.get("."),
repository,
mockk(),
)
val second =
RepoContext(
"second",
java.nio.file.Paths
.get("second"),
other,
mockk(),
)
every { it.current() } returns current
every { it.all() } returns listOf(current, second)
every { it.byName("second") } returns second
every { it.byName("nope") } returns null
}
private fun result(
branch: String,
@@ -32,7 +58,21 @@ class StatusCommandTest : FunSpec() {
init {
beforeEach {
clearMocks(repository)
clearMocks(repository, other)
}
test("--repo selects a registered repository; an unknown name is a usage error naming the registered ones") {
every { other.latestPerName() } returns listOf(result("main", BuildStatus.SUCCESS))
var exitCode = -1
val console = captureConsole { exitCode = StatusCommand(registry).apply { repoOption.name = "second" }.call() }
exitCode shouldBe 0
console.stdout shouldContain "main"
verify(exactly = 0) { repository.latestPerName() }
val failed = captureConsole { exitCode = StatusCommand(registry).apply { repoOption.name = "nope" }.call() }
exitCode shouldBe 2
failed.stderr shouldContain "current, second"
}
test("prints the latest build per branch as a table with short commits and legacy duration format") {
@@ -43,7 +83,7 @@ class StatusCommandTest : FunSpec() {
)
var exitCode = -1
val console = captureConsole { exitCode = StatusCommand(repository).call() }
val console = captureConsole { exitCode = StatusCommand(registry).call() }
exitCode shouldBe 0
console.stdout shouldContain "BRANCH"
@@ -64,7 +104,7 @@ class StatusCommandTest : FunSpec() {
result("main", BuildStatus.FAILED),
)
val command = StatusCommand(repository).apply { history = true }
val command = StatusCommand(registry).apply { history = true }
var exitCode = -1
val console = captureConsole { exitCode = command.call() }
@@ -78,7 +118,7 @@ class StatusCommandTest : FunSpec() {
every { repository.latestPerName() } returns emptyList()
var exitCode = -1
val console = captureConsole { exitCode = StatusCommand(repository).call() }
val console = captureConsole { exitCode = StatusCommand(registry).call() }
exitCode shouldBe 0
console.stdout shouldContain "(no builds recorded)"