diff --git a/src/main/kotlin/de/hoennig/werkator/commands/BuildCommand.kt b/src/main/kotlin/de/hoennig/werkator/commands/BuildCommand.kt index a465777..08a58e3 100644 --- a/src/main/kotlin/de/hoennig/werkator/commands/BuildCommand.kt +++ b/src/main/kotlin/de/hoennig/werkator/commands/BuildCommand.kt @@ -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 { + @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 diff --git a/src/main/kotlin/de/hoennig/werkator/commands/RepoOption.kt b/src/main/kotlin/de/hoennig/werkator/commands/RepoOption.kt new file mode 100644 index 0000000..1bab759 --- /dev/null +++ b/src/main/kotlin/de/hoennig/werkator/commands/RepoOption.kt @@ -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 = "", + 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 }})", + ) + } +} diff --git a/src/main/kotlin/de/hoennig/werkator/commands/RetryCommand.kt b/src/main/kotlin/de/hoennig/werkator/commands/RetryCommand.kt index a92092f..10f9d31 100644 --- a/src/main/kotlin/de/hoennig/werkator/commands/RetryCommand.kt +++ b/src/main/kotlin/de/hoennig/werkator/commands/RetryCommand.kt @@ -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 { + @Mixin + var repoOption = RepoOption() + + private lateinit var repo: RepoContext + private val workingDir: Path get() = repo.workingDir override fun call(): Int { val failed: List try { + repo = repoOption.select(registry) fetchBestEffort() failed = repo.results.latestPerName().filter { it.status == BuildStatus.FAILED } } catch (e: Exception) { diff --git a/src/main/kotlin/de/hoennig/werkator/commands/StatusCommand.kt b/src/main/kotlin/de/hoennig/werkator/commands/StatusCommand.kt index 29d1b21..63fbe61 100644 --- a/src/main/kotlin/de/hoennig/werkator/commands/StatusCommand.kt +++ b/src/main/kotlin/de/hoennig/werkator/commands/StatusCommand.kt @@ -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 { @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)") diff --git a/src/test/kotlin/de/hoennig/werkator/commands/BuildCommandTest.kt b/src/test/kotlin/de/hoennig/werkator/commands/BuildCommandTest.kt index 642ae28..0b4dc8e 100644 --- a/src/test/kotlin/de/hoennig/werkator/commands/BuildCommandTest.kt +++ b/src/test/kotlin/de/hoennig/werkator/commands/BuildCommandTest.kt @@ -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() private val dir: Path = Paths.get(".") private val repo = RepoContext("test", dir, mockk(), mockk()) + private val registry = mockk().also { every { it.current() } returns repo } private fun command(fragment: String? = null) = - BuildCommand(gitService, consoleBuildRunner, repo).apply { + BuildCommand(gitService, consoleBuildRunner, registry).apply { branchFragment = fragment } diff --git a/src/test/kotlin/de/hoennig/werkator/commands/RetryCommandTest.kt b/src/test/kotlin/de/hoennig/werkator/commands/RetryCommandTest.kt index 4c01f3e..4eb55dd 100644 --- a/src/test/kotlin/de/hoennig/werkator/commands/RetryCommandTest.kt +++ b/src/test/kotlin/de/hoennig/werkator/commands/RetryCommandTest.kt @@ -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() private val dir: Path = Paths.get(".") private val repo = RepoContext("test", dir, repository, mockk()) + private val registry = mockk().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, diff --git a/src/test/kotlin/de/hoennig/werkator/commands/StatusCommandTest.kt b/src/test/kotlin/de/hoennig/werkator/commands/StatusCommandTest.kt index de09035..6bf8bdf 100644 --- a/src/test/kotlin/de/hoennig/werkator/commands/StatusCommandTest.kt +++ b/src/test/kotlin/de/hoennig/werkator/commands/StatusCommandTest.kt @@ -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() + private val other = mockk() + private val registry = + mockk().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)"