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:
co-authored by
Claude Fable 5.1
parent
a5027acfaf
commit
b66d26a03a
@@ -3,9 +3,11 @@ package de.hoennig.werkator.commands
|
|||||||
import de.hoennig.werkator.build.BuildStatus
|
import de.hoennig.werkator.build.BuildStatus
|
||||||
import de.hoennig.werkator.git.GitService
|
import de.hoennig.werkator.git.GitService
|
||||||
import de.hoennig.werkator.repo.RepoContext
|
import de.hoennig.werkator.repo.RepoContext
|
||||||
|
import de.hoennig.werkator.repo.RepoRegistry
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import picocli.CommandLine.Command
|
import picocli.CommandLine.Command
|
||||||
import picocli.CommandLine.ExitCode
|
import picocli.CommandLine.ExitCode
|
||||||
|
import picocli.CommandLine.Mixin
|
||||||
import picocli.CommandLine.Parameters
|
import picocli.CommandLine.Parameters
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
@@ -24,9 +26,11 @@ import java.util.concurrent.Callable
|
|||||||
class BuildCommand(
|
class BuildCommand(
|
||||||
private val gitService: GitService,
|
private val gitService: GitService,
|
||||||
private val consoleBuildRunner: ConsoleBuildRunner,
|
private val consoleBuildRunner: ConsoleBuildRunner,
|
||||||
/** The repository to build: the current working directory (a repo selector comes with the registry). */
|
private val registry: RepoRegistry,
|
||||||
var repo: RepoContext,
|
|
||||||
) : Callable<Int> {
|
) : Callable<Int> {
|
||||||
|
@Mixin
|
||||||
|
var repoOption = RepoOption()
|
||||||
|
|
||||||
@Parameters(
|
@Parameters(
|
||||||
index = "0",
|
index = "0",
|
||||||
arity = "0..1",
|
arity = "0..1",
|
||||||
@@ -35,6 +39,8 @@ class BuildCommand(
|
|||||||
)
|
)
|
||||||
var branchFragment: String? = null
|
var branchFragment: String? = null
|
||||||
|
|
||||||
|
private lateinit var repo: RepoContext
|
||||||
|
|
||||||
private val workingDir: Path
|
private val workingDir: Path
|
||||||
get() = repo.workingDir
|
get() = repo.workingDir
|
||||||
|
|
||||||
@@ -42,6 +48,7 @@ class BuildCommand(
|
|||||||
val branch: String
|
val branch: String
|
||||||
val commit: String
|
val commit: String
|
||||||
try {
|
try {
|
||||||
|
repo = repoOption.select(registry)
|
||||||
fetchBestEffort()
|
fetchBestEffort()
|
||||||
branch = resolveBranch() ?: return ExitCode.USAGE
|
branch = resolveBranch() ?: return ExitCode.USAGE
|
||||||
commit = commitToBuild(branch) ?: 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.build.BuildStatus
|
||||||
import de.hoennig.werkator.git.GitService
|
import de.hoennig.werkator.git.GitService
|
||||||
import de.hoennig.werkator.repo.RepoContext
|
import de.hoennig.werkator.repo.RepoContext
|
||||||
|
import de.hoennig.werkator.repo.RepoRegistry
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import picocli.CommandLine.Command
|
import picocli.CommandLine.Command
|
||||||
import picocli.CommandLine.ExitCode
|
import picocli.CommandLine.ExitCode
|
||||||
|
import picocli.CommandLine.Mixin
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
|
|
||||||
@@ -25,15 +27,20 @@ import java.util.concurrent.Callable
|
|||||||
class RetryCommand(
|
class RetryCommand(
|
||||||
private val gitService: GitService,
|
private val gitService: GitService,
|
||||||
private val consoleBuildRunner: ConsoleBuildRunner,
|
private val consoleBuildRunner: ConsoleBuildRunner,
|
||||||
/** The repository to retry in: the current working directory (a repo selector comes with the registry). */
|
private val registry: RepoRegistry,
|
||||||
var repo: RepoContext,
|
|
||||||
) : Callable<Int> {
|
) : Callable<Int> {
|
||||||
|
@Mixin
|
||||||
|
var repoOption = RepoOption()
|
||||||
|
|
||||||
|
private lateinit var repo: RepoContext
|
||||||
|
|
||||||
private val workingDir: Path
|
private val workingDir: Path
|
||||||
get() = repo.workingDir
|
get() = repo.workingDir
|
||||||
|
|
||||||
override fun call(): Int {
|
override fun call(): Int {
|
||||||
val failed: List<BuildResult>
|
val failed: List<BuildResult>
|
||||||
try {
|
try {
|
||||||
|
repo = repoOption.select(registry)
|
||||||
fetchBestEffort()
|
fetchBestEffort()
|
||||||
failed = repo.results.latestPerName().filter { it.status == BuildStatus.FAILED }
|
failed = repo.results.latestPerName().filter { it.status == BuildStatus.FAILED }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package de.hoennig.werkator.commands
|
package de.hoennig.werkator.commands
|
||||||
|
|
||||||
import de.hoennig.werkator.build.BuildResult
|
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 de.hoennig.werkator.server.UiFormats
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import picocli.CommandLine.Command
|
import picocli.CommandLine.Command
|
||||||
import picocli.CommandLine.ExitCode
|
import picocli.CommandLine.ExitCode
|
||||||
|
import picocli.CommandLine.Mixin
|
||||||
import picocli.CommandLine.Option
|
import picocli.CommandLine.Option
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
|
|
||||||
@@ -20,12 +21,22 @@ import java.util.concurrent.Callable
|
|||||||
mixinStandardHelpOptions = true,
|
mixinStandardHelpOptions = true,
|
||||||
)
|
)
|
||||||
class StatusCommand(
|
class StatusCommand(
|
||||||
private val repository: BuildResultRepository,
|
private val registry: RepoRegistry,
|
||||||
) : Callable<Int> {
|
) : Callable<Int> {
|
||||||
@Option(names = ["--history"], description = ["Print all recorded builds, not only the latest per branch"])
|
@Option(names = ["--history"], description = ["Print all recorded builds, not only the latest per branch"])
|
||||||
var history: Boolean = false
|
var history: Boolean = false
|
||||||
|
|
||||||
|
@Mixin
|
||||||
|
var repoOption = RepoOption()
|
||||||
|
|
||||||
override fun call(): Int {
|
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()
|
val results = if (history) repository.history() else repository.latestPerName()
|
||||||
if (results.isEmpty()) {
|
if (results.isEmpty()) {
|
||||||
println("(no builds recorded)")
|
println("(no builds recorded)")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package de.hoennig.werkator.commands
|
|||||||
import de.hoennig.werkator.build.BuildStatus
|
import de.hoennig.werkator.build.BuildStatus
|
||||||
import de.hoennig.werkator.git.GitService
|
import de.hoennig.werkator.git.GitService
|
||||||
import de.hoennig.werkator.repo.RepoContext
|
import de.hoennig.werkator.repo.RepoContext
|
||||||
|
import de.hoennig.werkator.repo.RepoRegistry
|
||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import io.kotest.matchers.string.shouldContain
|
import io.kotest.matchers.string.shouldContain
|
||||||
@@ -20,9 +21,10 @@ class BuildCommandTest : FunSpec() {
|
|||||||
private val consoleBuildRunner = mockk<ConsoleBuildRunner>()
|
private val consoleBuildRunner = mockk<ConsoleBuildRunner>()
|
||||||
private val dir: Path = Paths.get(".")
|
private val dir: Path = Paths.get(".")
|
||||||
private val repo = RepoContext("test", dir, mockk(), mockk())
|
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) =
|
private fun command(fragment: String? = null) =
|
||||||
BuildCommand(gitService, consoleBuildRunner, repo).apply {
|
BuildCommand(gitService, consoleBuildRunner, registry).apply {
|
||||||
branchFragment = fragment
|
branchFragment = fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import de.hoennig.werkator.build.BuildResultRepository
|
|||||||
import de.hoennig.werkator.build.BuildStatus
|
import de.hoennig.werkator.build.BuildStatus
|
||||||
import de.hoennig.werkator.git.GitService
|
import de.hoennig.werkator.git.GitService
|
||||||
import de.hoennig.werkator.repo.RepoContext
|
import de.hoennig.werkator.repo.RepoContext
|
||||||
|
import de.hoennig.werkator.repo.RepoRegistry
|
||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import io.kotest.matchers.string.shouldContain
|
import io.kotest.matchers.string.shouldContain
|
||||||
@@ -24,8 +25,9 @@ class RetryCommandTest : FunSpec() {
|
|||||||
private val consoleBuildRunner = mockk<ConsoleBuildRunner>()
|
private val consoleBuildRunner = mockk<ConsoleBuildRunner>()
|
||||||
private val dir: Path = Paths.get(".")
|
private val dir: Path = Paths.get(".")
|
||||||
private val repo = RepoContext("test", dir, repository, mockk())
|
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(
|
private fun result(
|
||||||
branch: String,
|
branch: String,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package de.hoennig.werkator.commands
|
|||||||
import de.hoennig.werkator.build.BuildResult
|
import de.hoennig.werkator.build.BuildResult
|
||||||
import de.hoennig.werkator.build.BuildResultRepository
|
import de.hoennig.werkator.build.BuildResultRepository
|
||||||
import de.hoennig.werkator.build.BuildStatus
|
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.core.spec.style.FunSpec
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import io.kotest.matchers.string.shouldContain
|
import io.kotest.matchers.string.shouldContain
|
||||||
@@ -16,6 +18,30 @@ import java.time.Instant
|
|||||||
|
|
||||||
class StatusCommandTest : FunSpec() {
|
class StatusCommandTest : FunSpec() {
|
||||||
private val repository = mockk<BuildResultRepository>()
|
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(
|
private fun result(
|
||||||
branch: String,
|
branch: String,
|
||||||
@@ -32,7 +58,21 @@ class StatusCommandTest : FunSpec() {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
beforeEach {
|
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") {
|
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
|
var exitCode = -1
|
||||||
val console = captureConsole { exitCode = StatusCommand(repository).call() }
|
val console = captureConsole { exitCode = StatusCommand(registry).call() }
|
||||||
|
|
||||||
exitCode shouldBe 0
|
exitCode shouldBe 0
|
||||||
console.stdout shouldContain "BRANCH"
|
console.stdout shouldContain "BRANCH"
|
||||||
@@ -64,7 +104,7 @@ class StatusCommandTest : FunSpec() {
|
|||||||
result("main", BuildStatus.FAILED),
|
result("main", BuildStatus.FAILED),
|
||||||
)
|
)
|
||||||
|
|
||||||
val command = StatusCommand(repository).apply { history = true }
|
val command = StatusCommand(registry).apply { history = true }
|
||||||
var exitCode = -1
|
var exitCode = -1
|
||||||
val console = captureConsole { exitCode = command.call() }
|
val console = captureConsole { exitCode = command.call() }
|
||||||
|
|
||||||
@@ -78,7 +118,7 @@ class StatusCommandTest : FunSpec() {
|
|||||||
every { repository.latestPerName() } returns emptyList()
|
every { repository.latestPerName() } returns emptyList()
|
||||||
|
|
||||||
var exitCode = -1
|
var exitCode = -1
|
||||||
val console = captureConsole { exitCode = StatusCommand(repository).call() }
|
val console = captureConsole { exitCode = StatusCommand(registry).call() }
|
||||||
|
|
||||||
exitCode shouldBe 0
|
exitCode shouldBe 0
|
||||||
console.stdout shouldContain "(no builds recorded)"
|
console.stdout shouldContain "(no builds recorded)"
|
||||||
|
|||||||
Reference in New Issue
Block a user