From 716d5fa92981e9b1f195347d3e928dfb750da6bc Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Fri, 29 Mar 2024 10:03:30 +0100 Subject: [PATCH] EXPERIMENT: New Nessie CLI --- bom/build.gradle.kts | 2 + cli/cli/build.gradle.kts | 130 ++++ .../nessie/cli/cli/CommandsToRun.java | 88 +++ .../nessie/cli/cli/ConnectOptions.java | 47 ++ .../nessie/cli/cli/NessieCli.java | 42 ++ .../nessie/cli/cli/NessieCliCompleter.java | 109 ++++ .../nessie/cli/cli/NessieCliHighlighter.java | 137 ++++ .../nessie/cli/cli/NessieCliImpl.java | 592 ++++++++++++++++++ .../nessie/cli/cli/NessieVersionProvider.java | 26 + .../nessie/cli/cli/NotConnectedException.java | 22 + .../cli/commands/AssignReferenceCommand.java | 52 ++ .../nessie/cli/commands/CommandsFactory.java | 47 ++ .../nessie/cli/commands/ConnectCommand.java | 143 +++++ .../cli/commands/CreateReferenceCommand.java | 67 ++ .../cli/commands/DropReferenceCommand.java | 68 ++ .../nessie/cli/commands/ExitCommand.java | 48 ++ .../nessie/cli/commands/HelpCommand.java | 85 +++ .../nessie/cli/commands/LinesInputStream.java | 78 +++ .../cli/commands/ListReferencesCommand.java | 77 +++ .../cli/commands/MergeBranchCommand.java | 49 ++ .../nessie/cli/commands/NessieCommand.java | 34 + .../cli/commands/NessieListingCommand.java | 88 +++ .../nessie/cli/commands/ShowLogCommand.java | 87 +++ .../cli/commands/ShowReferenceCommand.java | 52 ++ .../cli/commands/UseReferenceCommand.java | 57 ++ .../src/main/resources/application.properties | 37 ++ .../projectnessie/nessie/cli/cli/banner.txt | 8 + .../projectnessie/nessie/cli/cli/welcome.txt | 8 + .../cli/commands/TestLinesInputStream.java | 119 ++++ cli/grammar/build.gradle.kts | 102 +++ .../cli/grammar/NessieCliParserBench.java | 67 ++ .../src/main/congocc/nessie-cli-java.ccc | 284 +++++++++ .../src/main/congocc/nessie-cli-lexer.ccc | 76 +++ cli/grammar/src/main/congocc/nessie-cli.ccc | 210 +++++++ .../cmdspec/AssignReferenceCommandSpec.java | 30 + .../nessie/cli/cmdspec/CommandContainer.java | 76 +++ .../nessie/cli/cmdspec/CommandSpec.java | 33 + .../nessie/cli/cmdspec/CommandType.java | 30 + .../cli/cmdspec/ConnectCommandSpec.java | 35 ++ .../cmdspec/CreateReferenceCommandSpec.java | 28 + .../cli/cmdspec/DropReferenceCommandSpec.java | 28 + .../nessie/cli/cmdspec/ExitCommandSpec.java | 25 + .../nessie/cli/cmdspec/HelpCommandSpec.java | 29 + .../cmdspec/ListReferencesCommandSpec.java | 25 + .../cli/cmdspec/MergeBranchCommandSpec.java | 29 + .../nessie/cli/cmdspec/RefCommandSpec.java | 22 + .../cli/cmdspec/RefWithTypeCommandSpec.java | 22 + .../cli/cmdspec/ShowLogCommandSpec.java | 30 + .../cli/cmdspec/ShowReferenceCommandSpec.java | 30 + .../cli/cmdspec/UseReferenceCommandSpec.java | 29 + .../nessie/cli/cmdspec/package-info.java | 20 + .../nessie/cli/completer/CliCompleter.java | 184 ++++++ .../nessie/cli/grammar/CompletionType.java | 22 + .../cli/grammar/IdentifierOrLiteral.java | 20 + .../cli/completer/TestCliCompleter.java | 569 +++++++++++++++++ gradle/libs.versions.toml | 3 + gradle/projects.main.properties | 2 + 57 files changed, 4459 insertions(+) create mode 100644 cli/cli/build.gradle.kts create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/CommandsToRun.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/ConnectOptions.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCli.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliCompleter.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliHighlighter.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliImpl.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieVersionProvider.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NotConnectedException.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/AssignReferenceCommand.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/CommandsFactory.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ConnectCommand.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/CreateReferenceCommand.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/DropReferenceCommand.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ExitCommand.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/HelpCommand.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/LinesInputStream.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ListReferencesCommand.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/MergeBranchCommand.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/NessieCommand.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/NessieListingCommand.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ShowLogCommand.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ShowReferenceCommand.java create mode 100644 cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/UseReferenceCommand.java create mode 100644 cli/cli/src/main/resources/application.properties create mode 100644 cli/cli/src/main/resources/org/projectnessie/nessie/cli/cli/banner.txt create mode 100644 cli/cli/src/main/resources/org/projectnessie/nessie/cli/cli/welcome.txt create mode 100644 cli/cli/src/test/java/org/projectnessie/nessie/cli/commands/TestLinesInputStream.java create mode 100644 cli/grammar/build.gradle.kts create mode 100644 cli/grammar/src/jmh/java/org/projectnessie/nessie/cli/grammar/NessieCliParserBench.java create mode 100644 cli/grammar/src/main/congocc/nessie-cli-java.ccc create mode 100644 cli/grammar/src/main/congocc/nessie-cli-lexer.ccc create mode 100644 cli/grammar/src/main/congocc/nessie-cli.ccc create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/AssignReferenceCommandSpec.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/CommandContainer.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/CommandSpec.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/CommandType.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ConnectCommandSpec.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/CreateReferenceCommandSpec.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/DropReferenceCommandSpec.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ExitCommandSpec.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/HelpCommandSpec.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ListReferencesCommandSpec.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/MergeBranchCommandSpec.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/RefCommandSpec.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/RefWithTypeCommandSpec.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ShowLogCommandSpec.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ShowReferenceCommandSpec.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/UseReferenceCommandSpec.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/package-info.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/completer/CliCompleter.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/grammar/CompletionType.java create mode 100644 cli/grammar/src/main/java/org/projectnessie/nessie/cli/grammar/IdentifierOrLiteral.java create mode 100644 cli/grammar/src/test/java/org/projectnessie/nessie/cli/completer/TestCliCompleter.java diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts index b7b4a209d2a..ecfa4b309ef 100644 --- a/bom/build.gradle.kts +++ b/bom/build.gradle.kts @@ -27,6 +27,8 @@ dependencies { constraints { api(rootProject) api(project(":nessie-azurite-testcontainer")) + api(project(":nessie-cli")) + api(project(":nessie-cli-grammar")) api(project(":nessie-client")) api(project(":nessie-client-testextension")) api(project(":nessie-combined-cs")) diff --git a/cli/cli/build.gradle.kts b/cli/cli/build.gradle.kts new file mode 100644 index 00000000000..6e846dd1107 --- /dev/null +++ b/cli/cli/build.gradle.kts @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.quarkus.gradle.tasks.QuarkusBuild +import org.apache.tools.ant.taskdefs.condition.Os + +plugins { + alias(libs.plugins.quarkus) + id("nessie-conventions-quarkus") + id("nessie-jacoco") +} + +extra["maven.name"] = "Nessie - CLI" + +dependencies { + implementation(project(":nessie-model")) + implementation(project(":nessie-client")) + implementation(project(":nessie-cli-grammar")) + + implementation(enforcedPlatform(libs.quarkus.bom)) + implementation("io.quarkus:quarkus-picocli") + implementation(libs.jline) + implementation(libs.jansi) + implementation(libs.picocli) + implementation(libs.caffeine) + + compileOnly(libs.immutables.value.annotations) + annotationProcessor(libs.immutables.value.processor) + + compileOnly(libs.jakarta.annotation.api) + compileOnly(libs.microprofile.openapi) + + implementation(platform(libs.jackson.bom)) + implementation("com.fasterxml.jackson.core:jackson-databind") + implementation("com.fasterxml.jackson.core:jackson-annotations") + + runtimeOnly(libs.agroal.pool) + runtimeOnly(libs.h2) + runtimeOnly(libs.postgresql) + + compileOnly(libs.immutables.builder) + compileOnly(libs.immutables.value.annotations) + annotationProcessor(libs.immutables.value.processor) + + testFixturesApi(enforcedPlatform(libs.quarkus.bom)) + testFixturesApi("io.quarkus:quarkus-junit5") + testFixturesApi(libs.microprofile.openapi) + + testFixturesApi(platform(libs.junit.bom)) + testFixturesApi(libs.bundles.junit.testing) + + testCompileOnly(libs.immutables.value.annotations) +} + +tasks.withType().configureEach { + from("src/main/resources") { + expand("nessieVersion" to version) + duplicatesStrategy = DuplicatesStrategy.INCLUDE + } +} + +val packageType = quarkusPackageType() + +quarkus { + quarkusBuildProperties.put("quarkus.package.type", packageType) + // quarkusBuildProperties.put("quarkus.package.output-directory", + // project.layout.buildDirectory.dir("nessie-cli").get().toString()) + // quarkusBuildProperties.put("quarkus.package.output-name", "nessie-cli") + quarkusBuildProperties.put("quarkus.package.add-runner-suffix", "false") + // Pull manifest attributes from the "main" `jar` task to get the + // release-information into the jars generated by Quarkus. + quarkusBuildProperties.putAll( + provider { + tasks + .named("jar", Jar::class.java) + .get() + .manifest + .attributes + .map { e -> "quarkus.package.manifest.attributes.\"${e.key}\"" to e.value.toString() } + .toMap() + } + ) +} + +if (quarkusFatJar()) { + afterEvaluate { + publishing { + publications { + named("maven") { + val quarkusBuild = tasks.getByName("quarkusBuild") + artifact(quarkusBuild.runnerJar) { + classifier = "runner" + builtBy(quarkusBuild) + } + } + } + } + } +} + +listOf("javadoc", "sourcesJar").forEach { name -> + tasks.named(name).configure { dependsOn(tasks.named("compileQuarkusGeneratedSourcesJava")) } +} + +listOf("checkstyleTest", "compileTestJava").forEach { name -> + tasks.named(name).configure { dependsOn(tasks.named("compileQuarkusTestGeneratedSourcesJava")) } +} + +// Testcontainers is not supported on Windows :( +if (Os.isFamily(Os.FAMILY_WINDOWS)) { + tasks.named("intTest").configure { this.enabled = false } +} + +// Issue w/ testcontainers/podman in GH workflows :( +if (Os.isFamily(Os.FAMILY_MAC) && System.getenv("CI") != null) { + tasks.named("intTest").configure { this.enabled = false } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/CommandsToRun.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/CommandsToRun.java new file mode 100644 index 00000000000..c52f6333419 --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/CommandsToRun.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cli; + +import static org.projectnessie.nessie.cli.cli.NessieCliImpl.OPTION_COMMAND; +import static org.projectnessie.nessie.cli.cli.NessieCliImpl.OPTION_CONTINUE_ON_ERROR; +import static org.projectnessie.nessie.cli.cli.NessieCliImpl.OPTION_KEEP_RUNNING; +import static org.projectnessie.nessie.cli.cli.NessieCliImpl.OPTION_SCRIPT; + +import java.util.List; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Option; + +class CommandsToRun { + + @ArgGroup CommandsSource commandsSource; + + @Option( + names = {"-K", OPTION_KEEP_RUNNING}, + description = { + "When running commands via the " + + OPTION_COMMAND + + " option the process will exit once the commands have been executed.", + "To keep the REPL running, specify this option." + }) + boolean keepRunning; + + @Option( + names = {"-E", OPTION_CONTINUE_ON_ERROR}, + description = { + "When running commands via the " + + OPTION_COMMAND + + " option the process will stop/exit when a command could not be parsed or ran into an error.", + "Specifying this option lets the REPL continue executing the remaining commands after parse or runtime errors." + }) + boolean continueOnError; + + @Override + public String toString() { + return "CommandsToRun{" + + "commandsSource=" + + commandsSource + + ", keepRunning=" + + keepRunning + + ", continueOnError=" + + continueOnError + + '}'; + } + + static class CommandsSource { + @Option( + names = {"-s", OPTION_SCRIPT}, + description = { + "Run the commands in the Nessie CLI script referenced by this option.", + "Possible values are either a file path or use the minus character ('-') to read the script from stdin." + }) + String scriptFile; + + @Option( + names = {"-c", OPTION_COMMAND}, + arity = "*", + description = { + "Nessie CLI commands to run. Each value represents one command.", + "The process will exit once all specified commands have been executed. To keep the REPL running, specify the " + + OPTION_KEEP_RUNNING + + " option." + }) + List commands = List.of(); + + @Override + public String toString() { + return "CommandsSource{" + "runScript='" + scriptFile + '\'' + ", commands=" + commands + '}'; + } + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/ConnectOptions.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/ConnectOptions.java new file mode 100644 index 00000000000..c2d80a74641 --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/ConnectOptions.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cli; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import picocli.CommandLine.Option; + +class ConnectOptions { + @Option( + names = {"-u", "--uri"}, + required = true, + description = "REST API endpoint URI to connect to.") + URI uri; + + @Option( + names = "--client-name", + description = + "Name of the client implementation to use, defaults to HTTP suitable for Nessie REST API.") + String clientName; + + @Option( + names = {"-o", "--client-option"}, + description = "Parameters to configure the REST client.", + split = ",", + arity = "0..*") + Map clientOptions = new HashMap<>(); + + @Option( + names = {"-r", "--initial-reference"}, + description = "Name of the Nessie reference to use.") + String initialReference; +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCli.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCli.java new file mode 100644 index 00000000000..26084ca8a1d --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCli.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cli; + +import java.io.PrintWriter; +import java.util.Optional; +import org.jline.terminal.Terminal; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.model.Reference; + +public interface NessieCli { + PrintWriter writer(); + + String readResource(String resource); + + void exitRepl(int exitCode); + + Terminal terminal(); + + void connected(NessieApiV2 nessieApi); + + void setCurrentReference(Reference reference); + + Reference getCurrentReference(); + + Optional nessieApi(); + + NessieApiV2 mandatoryNessieApi(); +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliCompleter.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliCompleter.java new file mode 100644 index 00000000000..bfd2bbb2aab --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliCompleter.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cli; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.jline.reader.Candidate; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; +import org.jline.reader.impl.completer.SystemCompleter; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.Reference; +import org.projectnessie.nessie.cli.completer.CliCompleter; +import org.projectnessie.nessie.cli.grammar.CompletionType; +import org.projectnessie.nessie.cli.grammar.NessieCliParser; + +class NessieCliCompleter extends SystemCompleter { + + private final NessieCli nessieCli; + + NessieCliCompleter(NessieCli nessieCli) { + this.nessieCli = nessieCli; + } + + @Override + public void complete(LineReader reader, ParsedLine line, List candidates) { + new CliCompleter(line.line(), line.cursor(), NessieCliParser::SingleStatement) { + @Override + protected void completeWithLiteral( + CompletionType completionType, String preceding, String toComplete, boolean quoted) { + switch (completionType) { + case REFERENCE_NAME -> { + AtomicInteger counter = new AtomicInteger(); + // TODO providing all (think: too many) reference names can "overwhelm" completion. + // JLine then warns with something like 'JLine terminal: do you wish to see all 218 + // possibilities (14 lines)?', which is not nice and does not help. + // The implementation should not return "too many" options but enough (starting + // strings) to continue completion, so users do not have to see that warning. + // TODO also pass 'toComplete', if not empty, as a filter down to Nessie + // TODO also pass the reference type, if given, as a filter down to Nessie (need to know + // the statement parsed up to here :( ) + nessieCli + .nessieApi() + .ifPresent( + api -> { + try { + api.getAllReferences().stream() + .map(Reference::getName) + .forEach(refName -> candidate(refName, counter.getAndIncrement())); + } catch (NessieNotFoundException e) { + throw new RuntimeException(e); + } + }); + } + case REFERENCE_SPEC -> { + // TODO add matching reference names :) + } + case NON_EXISTING_REFERENCE_NAME -> { + // cannot suggest something that does not exist + } + default -> { + // do nothing - don't break anything + } + } + } + + @Override + protected void tokenCandidateStartsWith(String preceding, String toComplete) { + candidate(toComplete, 1_000); + } + + @Override + protected void tokenCandidateContains(String preceding, String toComplete) { + candidate(toComplete, 2_000); + } + + @Override + protected void tokenCandidateOther(String preceding, String toComplete) { + candidate(toComplete, 3_000); + } + + private void candidate(String tokenString, int offset) { + candidates.add( + new Candidate( + tokenString, + tokenString, + null, + null, + null, + null, + true, + offset + candidates.size())); + } + }.tryStatement(); + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliHighlighter.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliHighlighter.java new file mode 100644 index 00000000000..e9316ff116b --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliHighlighter.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cli; + +import static org.jline.utils.AttributedStyle.DEFAULT; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import java.util.regex.Pattern; +import org.jline.reader.Highlighter; +import org.jline.reader.LineReader; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; +import org.projectnessie.nessie.cli.grammar.NessieCliLexer; +import org.projectnessie.nessie.cli.grammar.NessieCliParser; +import org.projectnessie.nessie.cli.grammar.ParseException; +import org.projectnessie.nessie.cli.grammar.Token; +import org.projectnessie.nessie.cli.grammar.ast.Command; +import org.projectnessie.nessie.cli.grammar.ast.Keyword; +import org.projectnessie.nessie.cli.grammar.ast.Literal; +import org.projectnessie.nessie.cli.grammar.ast.ReferenceTypes; + +class NessieCliHighlighter implements Highlighter { + + private static final AttributedStyle STYLE_COMMAND = DEFAULT.foreground(0, 128, 0); + private static final AttributedStyle STYLE_REF_TYPE = DEFAULT.foreground(0, 128, 128); + private static final AttributedStyle STYLE_KEYWORD = DEFAULT.foreground(0, 0, 128); + private static final AttributedStyle STYLE_INVALID = DEFAULT.foreground(168, 0, 0); + private static final AttributedStyle STYLE_LITERAL = DEFAULT.bold().italic(); + private static final AttributedStyle STYLE_OTHER = DEFAULT; + + /** + * Cache holding the highlighted representations of quite some recent inputs. + * + *

Parsing the input via the lexer/parser is not very expensive, usually in the range of less + * than 10 µs and less than 5 kB heap churn, 15 µs and 20 kB max for quite big Nessie CLI + * statements. Constructing the {@link AttributedString} is a bit more costly though, although + * humans would probably never realize. + */ + private final LoadingCache highlightCache; + + NessieCliHighlighter() { + this.highlightCache = Caffeine.newBuilder().maximumSize(250).build(this::highlight); + } + + private AttributedString highlight(String input) { + AttributedStringBuilder sb = new AttributedStringBuilder(); + int bufferIndex = 0; + + NessieCliLexer lexer = new NessieCliLexer(input); + NessieCliParser parser = new NessieCliParser(lexer); + try { + parser.SingleStatement(); + } catch (ParseException e) { + // ignore + } + + // Seek to beginning + int index = 0; + for (; ; index--) { + Token t = parser.getToken(index); + if (t == null) { + index++; + break; + } + } + + // Iterate through all tokens + for (; ; index++) { + Token t = parser.getToken(index); + if (t.getType().isEOF()) { + break; + } + + if (t.getBeginOffset() == t.getEndOffset()) { + continue; + } + + if (bufferIndex < t.getBeginOffset()) { + sb.append(input.substring(bufferIndex, t.getBeginOffset()), STYLE_OTHER); + } + + String substring = input.substring(t.getBeginOffset(), t.getEndOffset()); + AttributedStyle style; + if (t.isInvalid()) { + style = STYLE_INVALID; + } else if (t instanceof Command) { + style = STYLE_COMMAND; + } else if (t instanceof Keyword) { + style = STYLE_KEYWORD; + } else if (t instanceof ReferenceTypes) { + style = STYLE_REF_TYPE; + } else if (t instanceof Literal) { + style = STYLE_LITERAL; + } else { + style = STYLE_OTHER; + } + sb.append(substring, style); + bufferIndex = t.getEndOffset(); + } + + if (bufferIndex < input.length()) { + sb.append(input.substring(bufferIndex), STYLE_COMMAND); + } + + return sb.toAttributedString(); + } + + @Override + public AttributedString highlight(LineReader reader, String buffer) { + return highlightCache.get(buffer); + } + + @Override + public void setErrorPattern(Pattern errorPattern) { + // System.err.println("ERROR PATTERN " + errorPattern); + } + + @Override + public void setErrorIndex(int errorIndex) { + // System.err.println("ERROR INDEX " + errorIndex); + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliImpl.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliImpl.java new file mode 100644 index 00000000000..e9621ba1c0d --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliImpl.java @@ -0,0 +1,592 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cli; + +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; +import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_CLIENT_NAME; +import static org.projectnessie.nessie.cli.commands.CommandsFactory.buildCommandInstance; + +import io.quarkus.picocli.runtime.annotations.TopCommand; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.UncheckedIOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.concurrent.Callable; +import org.jline.reader.EndOfFileException; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.MaskingCallback; +import org.jline.reader.UserInterruptException; +import org.jline.reader.impl.DefaultParser; +import org.jline.terminal.Size; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; +import org.projectnessie.api.NessieVersion; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.client.http.HttpClientException; +import org.projectnessie.client.rest.NessieServiceException; +import org.projectnessie.error.BaseNessieClientServerException; +import org.projectnessie.model.Branch; +import org.projectnessie.model.Detached; +import org.projectnessie.model.Reference; +import org.projectnessie.model.Tag; +import org.projectnessie.nessie.cli.cmdspec.CommandSpec; +import org.projectnessie.nessie.cli.cmdspec.ImmutableConnectCommandSpec; +import org.projectnessie.nessie.cli.commands.NessieCommand; +import org.projectnessie.nessie.cli.grammar.NessieCliLexer; +import org.projectnessie.nessie.cli.grammar.NessieCliParser; +import org.projectnessie.nessie.cli.grammar.Node; +import org.projectnessie.nessie.cli.grammar.ParseException; +import org.projectnessie.nessie.cli.grammar.Token; +import org.projectnessie.nessie.cli.grammar.ast.Script; +import org.projectnessie.nessie.cli.grammar.ast.SingleStatement; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +@TopCommand +@Command(mixinStandardHelpOptions = true, versionProvider = NessieVersionProvider.class) +public class NessieCliImpl implements Callable, NessieCli { + + public static final String OPTION_COMMAND = "--command"; + public static final String OPTION_SCRIPT = "--run-script"; + public static final String OPTION_QUIET = "--quiet"; + public static final String OPTION_KEEP_RUNNING = "--keep-running"; + public static final String OPTION_CONTINUE_ON_ERROR = "--continue-on-error"; + + public static final AttributedStyle STYLE_ERROR = AttributedStyle.DEFAULT.foreground(128, 0, 0); + public static final AttributedStyle STYLE_FAINT = AttributedStyle.DEFAULT.faint(); + public static final AttributedStyle STYLE_YELLOW = + AttributedStyle.DEFAULT.foreground(128, 128, 0); + public static final AttributedStyle STYLE_GREEN = AttributedStyle.DEFAULT.foreground(0, 128, 0); + public static final AttributedStyle STYLE_BLUE = AttributedStyle.DEFAULT.foreground(0, 0, 128); + + private Terminal terminal; + + private Integer exitWithCode; + + private NessieApiV2 nessieApi; + private Reference currentReference; + + private String prompt; + private String rightPrompt; + + @ArgGroup( + exclusive = false, + heading = + """ + + Statements to execute before or without running the REPL + ======================================================== + + """) + private CommandsToRun commandsToRun; + + @Option( + names = {"-q", OPTION_QUIET}, + description = {"Quiet option - omit the welcome and exit output."}) + private boolean quiet; + + @ArgGroup( + exclusive = false, + heading = + """ + + Connect options + =============== + + """) + private ConnectOptions connectOptions; + + public NessieCliImpl() { + updatePrompt(); + } + + @Override + public PrintWriter writer() { + return terminal.writer(); + } + + @Override + public void exitRepl(int exitCode) { + exitWithCode = exitCode; + } + + @Override + public Terminal terminal() { + return terminal; + } + + @Override + public void setCurrentReference(Reference currentReference) { + this.currentReference = currentReference; + updatePrompt(); + } + + @Override + public Reference getCurrentReference() { + if (nessieApi == null) { + throw new NotConnectedException(); + } + return currentReference; + } + + @Override + public void connected(NessieApiV2 nessieApi) { + if (this.nessieApi != null) { + try { + this.nessieApi.close(); + } catch (Exception e) { + terminal + .writer() + .println( + new AttributedStringBuilder() + .append("Failed to close the existing client: ") + .append(e.toString(), STYLE_ERROR)); + } + } + this.nessieApi = nessieApi; + updatePrompt(); + } + + @Override + public Optional nessieApi() { + return Optional.ofNullable(nessieApi); + } + + @Override + public NessieApiV2 mandatoryNessieApi() { + if (nessieApi == null) { + throw new NotConnectedException(); + } + return nessieApi; + } + + @Override + public Integer call() throws Exception { + this.terminal = TerminalBuilder.builder().build(); + + // hard coded terminal size when redirecting + if (terminal.getWidth() == 0 || terminal.getHeight() == 0) { + terminal.setSize(new Size(120, 40)); + } + + try { + PrintWriter writer = terminal.writer(); + + if (!quiet) { + writer.print(readResource("banner.txt")); + writer.printf("v%s%n%n", NessieVersion.NESSIE_VERSION); + writer.print(readResource("welcome.txt")); + } + + if (connectOptions != null) { + if (!connectTo(connectOptions)) { + return 1; + } + } + + CommandsToRun commandsToRun = this.commandsToRun; + + if (commandsToRun != null) { + runCommands(commandsToRun); + } + + if (commandsToRun == null || commandsToRun.keepRunning) { + runRepl(); + } + } finally { + flushOutput(); + } + + return exitWithCode; + } + + boolean connectTo(ConnectOptions connectOptions) { + if (connectOptions.uri == null) { + terminal + .writer() + .println( + new AttributedString( + "Command line option --uri is mandatory when using any of the 'Connect options'", + STYLE_ERROR.bold()) + .toAnsi()); + return false; + } + ImmutableConnectCommandSpec.Builder specBuilder = + ImmutableConnectCommandSpec.builder() + .uri(connectOptions.uri.toString()) + .initialReference(connectOptions.initialReference); + if (connectOptions.clientName != null) { + specBuilder.putParameter(CONF_NESSIE_CLIENT_NAME, connectOptions.clientName); + } + if (connectOptions.clientOptions != null) { + connectOptions.clientOptions.forEach(specBuilder::putParameter); + } + return executeCommand(null, specBuilder.build()); + } + + void runCommands(CommandsToRun commandsToRun) { + CommandsToRun.CommandsSource source = commandsToRun.commandsSource; + + if (source.scriptFile != null) { + if (!runScript(commandsToRun)) { + return; + } + } + + for (String command : source.commands) { + if (!quiet) { + terminal + .writer() + .println( + new AttributedStringBuilder() + .append("Nessie> ", STYLE_FAINT) + .append(command) + .toAnsi()); + } + + if (!parseAndExecuteSingleStatement(command)) { + if (!commandsToRun.continueOnError) { + exitWithCode = 1; + return; + } + } + } + } + + boolean runScript(CommandsToRun commandsToRun) { + CommandsToRun.CommandsSource source = commandsToRun.commandsSource; + + String scriptSource; + try { + if ("-".equals(source.scriptFile)) { + StringWriter sw = new StringWriter(); + terminal.reader().transferTo(sw); + scriptSource = sw.toString(); + } else { + scriptSource = Files.readString(Paths.get(source.scriptFile)); + } + } catch (IOException e) { + handleException(e, null, null); + return false; + } + + if (scriptSource.isBlank()) { + return true; + } + + return parseAndExecuteScript(scriptSource, commandsToRun.continueOnError, true); + } + + boolean parseAndExecuteScript( + String scriptSource, boolean continueOnError, boolean echoStatement) { + NessieCliLexer lexer = new NessieCliLexer(scriptSource); + NessieCliParser parser = new NessieCliParser(lexer); + + try { + parser.Script(); + Node root = parser.rootNode(); + Script script = (Script) root; + for (CommandSpec commandSpec : script.getCommandSpecs()) { + + if (echoStatement) { + Node node = commandSpec.sourceNode(); + if (node != null) { + String command = scriptSource.substring(node.getBeginOffset(), node.getEndOffset()); + terminal + .writer() + .println( + new AttributedStringBuilder() + .append("Nessie> ", STYLE_FAINT) + .append(command) + .toAnsi()); + } + } + + if (!executeCommand(scriptSource, commandSpec) && !continueOnError) { + return false; + } + } + return true; + } catch (ParseException e) { + handleParseException(scriptSource, e); + } + + return true; + } + + boolean parseAndExecuteSingleStatement(String line) { + NessieCliLexer lexer = new NessieCliLexer(line); + NessieCliParser parser = new NessieCliParser(lexer); + try { + parser.SingleStatement(); + Node root = parser.rootNode(); + SingleStatement singleStatement = (SingleStatement) root; + CommandSpec commandSpec = singleStatement.getCommandSpec(); + return executeCommand(line, commandSpec); + } catch (ParseException e) { + handleParseException(line, e); + } + + return false; + } + + boolean executeCommand(String source, CommandSpec commandSpec) { + PrintWriter writer = terminal.writer(); + + flushOutput(); + + try { + NessieCommand nessieCommand = buildCommandInstance(commandSpec.commandType()); + nessieCommand.execute(this, commandSpec); + + return true; + } catch (BaseNessieClientServerException | NotConnectedException e) { + AttributedStringBuilder errMsg = new AttributedStringBuilder(); + errMsg.append(e.getMessage(), STYLE_ERROR); + writer.println(errMsg.toAnsi(terminal)); + } catch (RuntimeException e) { + Throwable c = e.getCause(); + if (c instanceof BaseNessieClientServerException) { + AttributedStringBuilder errMsg = new AttributedStringBuilder(); + errMsg.append(e.getMessage(), STYLE_ERROR); + writer.println(errMsg.toAnsi(terminal)); + } else { + handleException(e, commandSpec, source); + } + } catch (Exception e) { + handleException(e, commandSpec, source); + } finally { + flushOutput(); + } + + return false; + } + + void handleException(Exception e, CommandSpec commandSpec, String source) { + AttributedStringBuilder errMsg = new AttributedStringBuilder(); + + if (commandSpec != null) { + Node node = commandSpec.sourceNode(); + + if (node != null) { + errMsg + .append( + format( + "Encountered an error executing the statement around line %d, column %d .. line %d column %d", + node.getBeginLine(), + node.getBeginColumn(), + node.getEndLine(), + node.getEndColumn()), + STYLE_ERROR) + .append("\n "); + if (source != null) { + errMsg.append( + source.substring(node.getBeginOffset(), node.getEndOffset()), STYLE_ERROR.italic()); + } + } + } + + errMsg.append("\n\n"); + if (!(e instanceof HttpClientException) + && !(e instanceof BaseNessieClientServerException) + && !(e instanceof NessieServiceException) + && !(e instanceof IllegalArgumentException)) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + stackTrace = stackTrace.substring(stackTrace.indexOf('\n') + 1); + errMsg + .append(e.toString(), STYLE_ERROR.italic().bold()) + .append('\n') + .append(stackTrace, AttributedStyle.DEFAULT.italic().bold()); + } else { + errMsg + .append(e.getClass().getSimpleName() + ": ", STYLE_ERROR.italic().faint()) + .append(e.getMessage(), STYLE_ERROR.italic().bold()); + String last = ""; + for (Throwable cause = e.getCause(); cause != null; cause = cause.getCause()) { + String m = cause.toString(); + if (m.equals(last)) { + continue; + } + errMsg.append("\n caused by: ").append(m, AttributedStyle.BOLD); + last = m; + } + } + errMsg.append("\n\n"); + + terminal.writer().println(errMsg.toAnsi(terminal)); + } + + void handleParseException(String input, ParseException e) { + Node.TerminalNode t = e.getToken(); + AttributedStringBuilder errMsg = new AttributedStringBuilder(); + + errMsg + .append( + format( + "Encountered an error parsing the statement around line %d, column %d .. line %d column %d", + t.getBeginLine(), t.getBeginColumn(), t.getEndLine(), t.getEndColumn()), + STYLE_ERROR) + .append("\n\nFound: ") + .append( + input.substring(t.getBeginOffset(), t.getEndOffset()), STYLE_ERROR.italic().underline()) + .append("\nExpected one of the following: "); + boolean first = true; + for (Token.TokenType type : e.getExpectedTokenTypes()) { + if (first) { + first = false; + } else { + errMsg.append(" , "); + } + errMsg.append(type.name(), STYLE_YELLOW.italic()); + } + errMsg + .append("\n\n") + .append(input.substring(0, t.getBeginOffset())) + .append( + input.substring(t.getBeginOffset(), t.getEndOffset()), + STYLE_ERROR.bold().italic().underline()) + .append(input.substring(t.getEndOffset())) + .append("\n\n"); + + terminal.writer().println(errMsg.toAnsi(terminal)); + } + + private void runRepl() { + + LineReader reader = + LineReaderBuilder.builder() + .terminal(terminal) + // + .parser( + new DefaultParser() + .blockCommentDelims(new DefaultParser.BlockCommentDelims("/*", "*/")) + .lineCommentDelims(new String[] {"//"})) + // + .completer(new NessieCliCompleter(this)) + // + .highlighter(new NessieCliHighlighter()) + // + .variable(LineReader.INDENTATION, 2) + .variable(LineReader.LIST_MAX, 100) + // .variable(LineReader.HISTORY_FILE, Paths.get(root, "history")) + // + // Nessie CLI syntax is case-insensitive + .option(LineReader.Option.CASE_INSENSITIVE, true) + .option(LineReader.Option.INSERT_BRACKET, true) + .option(LineReader.Option.EMPTY_WORD_OPTIONS, false) + .option( + LineReader.Option.USE_FORWARD_SLASH, + true) // use forward slash in directory separator + .option(LineReader.Option.DISABLE_EVENT_EXPANSION, true) + .build(); + + // TailTipWidgets tailTip = + // new TailTipWidgets( + // reader, + // cmdLine -> { + // terminal.writer().println(cmdLine.getDescriptionType() + " " + + // cmdLine.getArgs()); + // return null; + // }, + // 5, + // TailTipWidgets.TipType.COMBINED); + // tailTip.toggleKeyBindings(); + + while (exitWithCode == null) { + String line; + try { + line = reader.readLine(prompt, rightPrompt, (MaskingCallback) null, null); + } catch (UserInterruptException e) { + // ignore + continue; + } catch (EndOfFileException e) { + break; + } + + if (line.trim().isBlank()) { + continue; + } + + parseAndExecuteScript(line, false, false); + } + + if (!quiet) { + terminal + .writer() + .println( + """ + + Bye + """); + } + } + + @Override + public String readResource(String resource) { + URL url = NessieCliImpl.class.getResource(resource); + try (InputStream in = + requireNonNull(url, "Could not open resource from classpath " + resource) + .openConnection() + .getInputStream()) { + return new String(in.readAllBytes(), UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private void updatePrompt() { + AttributedStringBuilder prompt = new AttributedStringBuilder(); + AttributedStringBuilder rightPrompt = new AttributedStringBuilder(); + + if (nessieApi == null) { + prompt.append("Not connected - use 'CONNECT TO' statement\n", STYLE_YELLOW.bold()); + prompt.append("Nessie", STYLE_YELLOW.bold()); + } else { + if (currentReference instanceof Branch) { + prompt.append(currentReference.getName(), STYLE_GREEN.bold()); + } else if (currentReference instanceof Tag) { + prompt.append(currentReference.getName(), STYLE_BLUE.bold()); + } else if (currentReference instanceof Detached) { + if (currentReference.getHash() != null) { + prompt.append('@'); + prompt.append(currentReference.getHash(), STYLE_FAINT.italic()); + } + } + } + prompt.append("> "); + + this.prompt = prompt.toAnsi(); + this.rightPrompt = rightPrompt.toAnsi(terminal); + } + + void flushOutput() { + terminal.writer().flush(); + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieVersionProvider.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieVersionProvider.java new file mode 100644 index 00000000000..270c07b6e87 --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieVersionProvider.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cli; + +import org.projectnessie.api.NessieVersion; +import picocli.CommandLine.IVersionProvider; + +public class NessieVersionProvider implements IVersionProvider { + @Override + public String[] getVersion() { + return new String[] {NessieVersion.NESSIE_VERSION}; + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NotConnectedException.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NotConnectedException.java new file mode 100644 index 00000000000..5343b123133 --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NotConnectedException.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cli; + +public class NotConnectedException extends RuntimeException { + public NotConnectedException() { + super("Not connected to Nessie"); + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/AssignReferenceCommand.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/AssignReferenceCommand.java new file mode 100644 index 00000000000..4e70e3d13da --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/AssignReferenceCommand.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.commands; + +import jakarta.annotation.Nonnull; +import java.util.List; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.nessie.cli.cli.NessieCli; +import org.projectnessie.nessie.cli.cmdspec.AssignReferenceCommandSpec; +import org.projectnessie.nessie.cli.grammar.Node; +import org.projectnessie.nessie.cli.grammar.Token; + +public class AssignReferenceCommand extends NessieCommand { + public AssignReferenceCommand() {} + + @Override + public void execute(@Nonnull NessieCli nessieCli, AssignReferenceCommandSpec commandSpec) { + NessieApiV2 api = nessieCli.mandatoryNessieApi(); + + commandSpec.getRef(); + commandSpec.getRefType(); + } + + public String name() { + return Token.TokenType.ASSIGN + " " + Token.TokenType.BRANCH + "/" + Token.TokenType.TAG; + } + + public String description() { + return """ + Assign the tip of a branch or tag to a commit ID. + """; + } + + @Override + public boolean matchesForHelp(List helpArgs) { + Node arg0 = helpArgs.get(0); + return arg0 instanceof Token && arg0.getType() == Token.TokenType.ASSIGN; + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/CommandsFactory.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/CommandsFactory.java new file mode 100644 index 00000000000..a5159c6de35 --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/CommandsFactory.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.commands; + +import java.util.EnumMap; +import java.util.function.Supplier; +import org.projectnessie.nessie.cli.cmdspec.CommandSpec; +import org.projectnessie.nessie.cli.cmdspec.CommandType; + +public class CommandsFactory { + private static final EnumMap>> COMMMAND_FACTORIES; + + static { + COMMMAND_FACTORIES = new EnumMap<>(CommandType.class); + COMMMAND_FACTORIES.put(CommandType.CONNECT, ConnectCommand::new); + COMMMAND_FACTORIES.put(CommandType.ASSIGN_REFERENCE, AssignReferenceCommand::new); + COMMMAND_FACTORIES.put(CommandType.CREATE_REFERENCE, CreateReferenceCommand::new); + COMMMAND_FACTORIES.put(CommandType.DROP_REFERENCE, DropReferenceCommand::new); + COMMMAND_FACTORIES.put(CommandType.EXIT, ExitCommand::new); + COMMMAND_FACTORIES.put(CommandType.HELP, HelpCommand::new); + COMMMAND_FACTORIES.put(CommandType.LIST_REFERENCES, ListReferencesCommand::new); + COMMMAND_FACTORIES.put(CommandType.MERGE_BRANCH, MergeBranchCommand::new); + COMMMAND_FACTORIES.put(CommandType.SHOW_LOG, ShowLogCommand::new); + COMMMAND_FACTORIES.put(CommandType.SHOW_REFERENCE, ShowReferenceCommand::new); + COMMMAND_FACTORIES.put(CommandType.USE_REFERENCE, UseReferenceCommand::new); + } + + @SuppressWarnings({"rawtypes", "unchecked", "UnnecessaryLocalVariable"}) + public static NessieCommand buildCommandInstance( + CommandType commandType) { + NessieCommand cmd = COMMMAND_FACTORIES.get(commandType).get(); + return cmd; + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ConnectCommand.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ConnectCommand.java new file mode 100644 index 00000000000..f8743d4709b --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ConnectCommand.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.commands; + +import static java.lang.String.format; + +import jakarta.annotation.Nonnull; +import java.io.PrintWriter; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import org.jline.terminal.Terminal; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStyle; +import org.projectnessie.client.NessieClientBuilder; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.client.config.NessieClientConfigSources; +import org.projectnessie.model.NessieConfiguration; +import org.projectnessie.model.Reference; +import org.projectnessie.nessie.cli.cli.NessieCli; +import org.projectnessie.nessie.cli.cmdspec.ConnectCommandSpec; +import org.projectnessie.nessie.cli.grammar.Node; +import org.projectnessie.nessie.cli.grammar.Token; + +public class ConnectCommand extends NessieCommand { + public ConnectCommand() {} + + @Override + public void execute(@Nonnull NessieCli nessieCli, ConnectCommandSpec commandSpec) + throws Exception { + + PrintWriter writer = nessieCli.writer(); + + writer.println( + new AttributedString( + format("Connecting to %s ...", commandSpec.getUri()), AttributedStyle.DEFAULT.faint())); + writer.flush(); + + NessieApiV2 api = null; + NessieConfiguration config; + + AtomicReference cancellation = new AtomicReference<>(() -> {}); + + Terminal terminal = nessieCli.terminal(); + Terminal.SignalHandler sigIntHandler = + terminal.handle( + Terminal.Signal.INT, + sig -> { + // Do not block the SIGINT handler thread (it's an infrastructure thread!) + new Thread(cancellation.get(), "Cancel authentication").start(); + }); + try { + api = + NessieClientBuilder.createClientBuilderFromSystemSettings( + NessieClientConfigSources.mapConfigSource(commandSpec.getParameters())) + .withUri(commandSpec.getUri()) + .withCancellationCallback(cancellation::set) + .build(NessieApiV2.class); + + config = api.getConfig(); + } catch (Exception e) { + if (api != null) { + api.close(); + } + + if (hasCauseMatching( + e, + t -> + t instanceof CancellationException + || t instanceof InterruptedException + || t instanceof TimeoutException)) { + writer.println( + new AttributedString( + "Connection request aborted or timed out.", + AttributedStyle.DEFAULT.foreground(128, 128, 0)) + .toAnsi()); + writer.println(); + writer.flush(); + + return; + } + throw e; + } finally { + if (sigIntHandler != null) { + terminal.handle(Terminal.Signal.INT, sigIntHandler); + } + } + + writer.printf( + "Successfully connected to %s - Nessie API version %d, spec version %s%n", + commandSpec.getUri(), config.getActualApiVersion(), config.getSpecVersion()); + + nessieCli.connected(api); + + Reference ref; + if (commandSpec.getInitialReference() != null) { + ref = api.getReference().refName(commandSpec.getInitialReference()).get(); + } else { + ref = api.getDefaultBranch(); + } + nessieCli.setCurrentReference(ref); + } + + static boolean hasCauseMatching(Throwable t, Predicate test) { + for (; t != null; t = t.getCause()) { + if (test.test(t)) { + return true; + } + } + return false; + } + + public String name() { + return Token.TokenType.CONNECT.name(); + } + + public String description() { + return """ + Connect to a Nessie repository. + """; + } + + @Override + public boolean matchesForHelp(List helpArgs) { + Node arg0 = helpArgs.get(0); + return arg0 instanceof Token && arg0.getType() == Token.TokenType.CONNECT; + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/CreateReferenceCommand.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/CreateReferenceCommand.java new file mode 100644 index 00000000000..963a81b5770 --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/CreateReferenceCommand.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.commands; + +import jakarta.annotation.Nonnull; +import java.util.List; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.model.Branch; +import org.projectnessie.model.Reference; +import org.projectnessie.model.Tag; +import org.projectnessie.nessie.cli.cli.NessieCli; +import org.projectnessie.nessie.cli.cmdspec.CreateReferenceCommandSpec; +import org.projectnessie.nessie.cli.grammar.Node; +import org.projectnessie.nessie.cli.grammar.Token; + +public class CreateReferenceCommand extends NessieCommand { + public CreateReferenceCommand() {} + + @Override + public void execute(@Nonnull NessieCli nessieCli, CreateReferenceCommandSpec commandSpec) + throws Exception { + NessieApiV2 api = nessieCli.mandatoryNessieApi(); + + Reference currentRef = + api.getReference().refName(nessieCli.getCurrentReference().getName()).get(); + + Reference reference = + switch (Reference.ReferenceType.valueOf(commandSpec.getRefType())) { + case BRANCH -> Branch.of(commandSpec.getRef(), currentRef.getHash()); + case TAG -> Tag.of(commandSpec.getRef(), currentRef.getHash()); + default -> + throw new IllegalArgumentException( + "Unknown reference type: " + commandSpec.getRefType()); + }; + + api.createReference().reference(reference).sourceRefName(currentRef.getName()).create(); + } + + public String name() { + return Token.TokenType.CREATE + " " + Token.TokenType.BRANCH + "/" + Token.TokenType.TAG; + } + + public String description() { + return """ + Create a branch or tag. + """; + } + + @Override + public boolean matchesForHelp(List helpArgs) { + Node arg0 = helpArgs.get(0); + return arg0 instanceof Token && arg0.getType() == Token.TokenType.CREATE; + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/DropReferenceCommand.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/DropReferenceCommand.java new file mode 100644 index 00000000000..3d63492c663 --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/DropReferenceCommand.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.commands; + +import static java.lang.String.format; + +import jakarta.annotation.Nonnull; +import java.util.List; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.model.Reference; +import org.projectnessie.nessie.cli.cli.NessieCli; +import org.projectnessie.nessie.cli.cmdspec.DropReferenceCommandSpec; +import org.projectnessie.nessie.cli.grammar.Node; +import org.projectnessie.nessie.cli.grammar.Token; + +public class DropReferenceCommand extends NessieCommand { + public DropReferenceCommand() {} + + @Override + public void execute(@Nonnull NessieCli nessieCli, DropReferenceCommandSpec commandSpec) + throws Exception { + NessieApiV2 api = nessieCli.mandatoryNessieApi(); + + Reference reference = api.getReference().refName(commandSpec.getRef()).get(); + + if (Reference.ReferenceType.valueOf(commandSpec.getRefType()) != reference.getType()) { + throw new IllegalArgumentException( + format( + "'%s' is not a %s but a %s.", + commandSpec.getRef(), commandSpec.getRefType(), reference.getType())); + } + + if (commandSpec.getRef().equals(nessieCli.getCurrentReference().getName())) { + throw new IllegalArgumentException("Must not delete the current reference."); + } + + api.deleteReference().reference(reference).delete(); + } + + public String name() { + return Token.TokenType.DROP + " " + Token.TokenType.BRANCH + "/" + Token.TokenType.TAG; + } + + public String description() { + return """ + Delete a branch or tag. + """; + } + + @Override + public boolean matchesForHelp(List helpArgs) { + Node arg0 = helpArgs.get(0); + return arg0 instanceof Token && arg0.getType() == Token.TokenType.DROP; + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ExitCommand.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ExitCommand.java new file mode 100644 index 00000000000..b9bae822b5c --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ExitCommand.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.commands; + +import jakarta.annotation.Nonnull; +import java.util.List; +import org.projectnessie.nessie.cli.cli.NessieCli; +import org.projectnessie.nessie.cli.cmdspec.ExitCommandSpec; +import org.projectnessie.nessie.cli.grammar.Node; +import org.projectnessie.nessie.cli.grammar.Token; + +public class ExitCommand extends NessieCommand { + public ExitCommand() {} + + @Override + public void execute(@Nonnull NessieCli nessieCli, ExitCommandSpec commandSpec) { + nessieCli.exitRepl(0); + } + + public String name() { + return Token.TokenType.EXIT.name(); + } + + public String description() { + return """ + Exit the Nessie REPL. + """; + } + + @Override + public boolean matchesForHelp(List helpArgs) { + Node arg0 = helpArgs.get(0); + return arg0 instanceof Token && arg0.getType() == Token.TokenType.EXIT; + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/HelpCommand.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/HelpCommand.java new file mode 100644 index 00000000000..df10c365565 --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/HelpCommand.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.commands; + +import jakarta.annotation.Nonnull; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.List; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStyle; +import org.projectnessie.nessie.cli.cli.NessieCli; +import org.projectnessie.nessie.cli.cmdspec.CommandType; +import org.projectnessie.nessie.cli.cmdspec.HelpCommandSpec; +import org.projectnessie.nessie.cli.grammar.Node; +import org.projectnessie.nessie.cli.grammar.Token; + +public class HelpCommand extends NessieCommand { + public HelpCommand() {} + + @Override + public void execute(@Nonnull NessieCli nessieCli, HelpCommandSpec commandSpec) { + PrintWriter writer = nessieCli.writer(); + + writer.println( + """ + + Nessie CLI - Help + """); + + AttributedStyle styleCommandName = AttributedStyle.DEFAULT.bold(); + + List args = commandSpec.getArguments(); + + for (CommandType value : CommandType.values()) { + NessieCommand cmd = CommandsFactory.buildCommandInstance(value); + + if (!args.isEmpty() && !cmd.matchesForHelp(args)) { + continue; + } + + writer.println( + new AttributedString(cmd.name(), styleCommandName).toAnsi(nessieCli.terminal())); + Arrays.stream(cmd.description().split("\n")) + .map(l -> " " + l) + .forEach(writer::println); + writer.println(); + } + + // String command = commandSpec.getCommand(); + // if (command == null) { + // allCommandsHelp(writer); + // } else { + // commandHelp(command, writer); + // } + } + + public String name() { + return Token.TokenType.HELP.name(); + } + + public String description() { + return """ + Prints help information. + """; + } + + @Override + public boolean matchesForHelp(List helpArgs) { + Node arg0 = helpArgs.get(0); + return arg0 instanceof Token && arg0.getType() == Token.TokenType.HELP; + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/LinesInputStream.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/LinesInputStream.java new file mode 100644 index 00000000000..2f546a95440 --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/LinesInputStream.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.commands; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.InputStream; +import java.util.Spliterator; +import java.util.stream.Stream; + +public class LinesInputStream extends InputStream { + private final Spliterator source; + + public LinesInputStream(Stream source) { + this(source.spliterator()); + } + + public LinesInputStream(Spliterator source) { + this.source = source; + } + + private byte[] current = null; + private int pos; + private final byte[] single = new byte[1]; + + @Override + public int read(byte[] b, int off, int len) { + int rd = 0; + + while (len > 0) { + byte[] c = current; + if (c == null) { + if (!source.tryAdvance( + line -> { + current = (line + '\n').getBytes(UTF_8); + pos = 0; + })) { + return rd > 0 ? rd : -1; + } + c = current; + } + int remaining = c.length - pos; + int canPull = Math.min(remaining, len); + System.arraycopy(c, pos, b, off, canPull); + rd += canPull; + pos += canPull; + off += canPull; + len -= canPull; + if (pos == c.length) { + current = null; + } + } + + return rd; + } + + @Override + public int read() { + int rd = read(single, 0, 1); + if (rd <= 0) { + return -1; + } + return ((int) single[0]) & 0xff; + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ListReferencesCommand.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ListReferencesCommand.java new file mode 100644 index 00000000000..a8231568ab7 --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ListReferencesCommand.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.commands; + +import static java.lang.String.format; + +import java.util.List; +import java.util.stream.Stream; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; +import org.projectnessie.model.FetchOption; +import org.projectnessie.model.Reference; +import org.projectnessie.nessie.cli.cli.NessieCli; +import org.projectnessie.nessie.cli.cmdspec.ListReferencesCommandSpec; +import org.projectnessie.nessie.cli.grammar.Node; +import org.projectnessie.nessie.cli.grammar.Token; + +public class ListReferencesCommand extends NessieListingCommand { + public ListReferencesCommand() {} + + @Override + protected Stream exeucteListing( + NessieCli nessieCli, ListReferencesCommandSpec commandSpec) throws Exception { + FetchOption fetchOption = FetchOption.MINIMAL; + + Stream references = + nessieCli.mandatoryNessieApi().getAllReferences().fetch(fetchOption).stream(); + + Stream textual = + references.flatMap( + ref -> { + return Stream.of( + new AttributedStringBuilder() + .append( + format(" %-6s ", ref.getType().name()), + AttributedStyle.DEFAULT.faint()) // foreground(128, 128, 128)) + .append(ref.getName()) + .append(format(" @ %s", ref.getHash()), AttributedStyle.DEFAULT.faint()) + .toAnsi(nessieCli.terminal())); + + // ref.getMetadata().getNumCommitsAhead(); + // ref.getMetadata().getNumCommitsBehind(); + // ref.getMetadata().getNumTotalCommits(); + }); + + return textual; + } + + public String name() { + return Token.TokenType.LIST + " " + Token.TokenType.REFERENCES; + } + + public String description() { + return """ + List named references (branches and tags). + """; + } + + @Override + public boolean matchesForHelp(List helpArgs) { + Node arg0 = helpArgs.get(0); + return arg0 instanceof Token && arg0.getType() == Token.TokenType.LIST; + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/MergeBranchCommand.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/MergeBranchCommand.java new file mode 100644 index 00000000000..aef40855edc --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/MergeBranchCommand.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.commands; + +import jakarta.annotation.Nonnull; +import java.util.List; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.nessie.cli.cli.NessieCli; +import org.projectnessie.nessie.cli.cmdspec.MergeBranchCommandSpec; +import org.projectnessie.nessie.cli.grammar.Node; +import org.projectnessie.nessie.cli.grammar.Token; + +public class MergeBranchCommand extends NessieCommand { + public MergeBranchCommand() {} + + @Override + public void execute(@Nonnull NessieCli nessieCli, MergeBranchCommandSpec commandSpec) { + NessieApiV2 api = nessieCli.mandatoryNessieApi(); + } + + public String name() { + return Token.TokenType.MERGE + " " + Token.TokenType.BRANCH; + } + + public String description() { + return """ + Merge a reference into a branch. + """; + } + + @Override + public boolean matchesForHelp(List helpArgs) { + Node arg0 = helpArgs.get(0); + return arg0 instanceof Token && arg0.getType() == Token.TokenType.MERGE; + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/NessieCommand.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/NessieCommand.java new file mode 100644 index 00000000000..5b7fd856e29 --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/NessieCommand.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.commands; + +import jakarta.annotation.Nonnull; +import java.util.List; +import org.projectnessie.nessie.cli.cli.NessieCli; +import org.projectnessie.nessie.cli.cmdspec.CommandSpec; +import org.projectnessie.nessie.cli.grammar.Node; + +public abstract class NessieCommand { + protected NessieCommand() {} + + public abstract void execute(@Nonnull NessieCli cli, SPEC spec) throws Exception; + + public abstract String name(); + + public abstract String description(); + + public abstract boolean matchesForHelp(List helpArgs); +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/NessieListingCommand.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/NessieListingCommand.java new file mode 100644 index 00000000000..8e1ce291af8 --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/NessieListingCommand.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.commands; + +import jakarta.annotation.Nonnull; +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Stream; +import org.jline.builtins.Less; +import org.jline.builtins.Options; +import org.jline.builtins.Source; +import org.jline.terminal.Terminal; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.client.api.OnReferenceBuilder; +import org.projectnessie.nessie.cli.cli.NessieCli; +import org.projectnessie.nessie.cli.cmdspec.CommandSpec; +import org.projectnessie.nessie.cli.cmdspec.RefCommandSpec; + +public abstract class NessieListingCommand extends NessieCommand { + + public > B applyReference( + NessieCli nessieCli, SPEC commandSpec, B onReferenceBuilder) { + String refName = null; + if (commandSpec instanceof RefCommandSpec) { + refName = ((RefCommandSpec) commandSpec).getRef(); + } + if (refName == null) { + refName = nessieCli.getCurrentReference().getName(); + } + + return onReferenceBuilder.refName(refName); + } + + public void execute(@Nonnull NessieCli nessieCli, SPEC commandSpec) throws Exception { + + NessieApiV2 api = nessieCli.mandatoryNessieApi(); + + Terminal terminal = nessieCli.terminal(); + PrintStream ps = new PrintStream(terminal.output()); + try { + + Stream listing = exeucteListing(nessieCli, commandSpec); + + InputStream in = new LinesInputStream(listing); + + Less less = + new Less( + terminal, + Paths.get("."), + Options.compile(Less.usage()).parse(List.of("--quit-if-one-screen"))); + + // Capture Ctrl-C - no-op within 'less', because otherwise ctrl-C kills the whole process :( + Terminal.SignalHandler prevHandler = terminal.handle(Terminal.Signal.INT, sig -> {}); + try { + less.run(new Source.InputStreamSource(in, false, "Commit log")); + } finally { + if (prevHandler != null) { + terminal.handle(Terminal.Signal.INT, prevHandler); + } + } + + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + ps.flush(); + } + } + + protected abstract Stream exeucteListing(NessieCli nessieCli, SPEC commandSpec) + throws Exception; +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ShowLogCommand.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ShowLogCommand.java new file mode 100644 index 00000000000..291d08b3c37 --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ShowLogCommand.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.commands; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStyle; +import org.projectnessie.model.CommitMeta; +import org.projectnessie.model.FetchOption; +import org.projectnessie.model.LogResponse; +import org.projectnessie.nessie.cli.cli.NessieCli; +import org.projectnessie.nessie.cli.cmdspec.ShowLogCommandSpec; +import org.projectnessie.nessie.cli.grammar.Node; +import org.projectnessie.nessie.cli.grammar.Token; + +public class ShowLogCommand extends NessieListingCommand { + public ShowLogCommand() {} + + @Override + protected Stream exeucteListing(NessieCli nessieCli, ShowLogCommandSpec commandSpec) + throws Exception { + + FetchOption fetchOption = FetchOption.MINIMAL; + + Stream commitLog = + applyReference(nessieCli, commandSpec, nessieCli.mandatoryNessieApi().getCommitLog()) + .fetch(fetchOption) + .stream(); + + Stream textual = + commitLog.flatMap( + e -> { + CommitMeta meta = e.getCommitMeta(); + + Stream header = + Stream.of( + new AttributedString( + "commit " + meta.getHash(), + AttributedStyle.DEFAULT.foreground(128, 128, 0)) + .toAnsi(nessieCli.terminal()), + "Author: " + meta.getAuthor(), + "Date: " + meta.getAuthorTime(), + ""); + + Stream message = + Arrays.stream(meta.getMessage().split("\n")).map(s -> " " + s); + + return Stream.concat(header, Stream.concat(message, Stream.of(""))); + }); + + return textual; + } + + public String name() { + return Token.TokenType.SHOW + " " + Token.TokenType.LOG; + } + + public String description() { + return """ + List commits. + """; + } + + @Override + public boolean matchesForHelp(List helpArgs) { + Node arg0 = helpArgs.get(0); + Node arg1 = helpArgs.size() >= 2 ? helpArgs.get(1) : null; + return arg0 instanceof Token + && arg0.getType() == Token.TokenType.SHOW + && (arg1 == null || (arg1 instanceof Token && arg1.getType() == Token.TokenType.LOG)); + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ShowReferenceCommand.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ShowReferenceCommand.java new file mode 100644 index 00000000000..555b27fe3e5 --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/ShowReferenceCommand.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.commands; + +import jakarta.annotation.Nonnull; +import java.util.List; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.nessie.cli.cli.NessieCli; +import org.projectnessie.nessie.cli.cmdspec.ShowReferenceCommandSpec; +import org.projectnessie.nessie.cli.grammar.Node; +import org.projectnessie.nessie.cli.grammar.Token; + +public class ShowReferenceCommand extends NessieCommand { + public ShowReferenceCommand() {} + + @Override + public void execute(@Nonnull NessieCli nessieCli, ShowReferenceCommandSpec commandSpec) { + NessieApiV2 api = nessieCli.mandatoryNessieApi(); + } + + public String name() { + return Token.TokenType.SHOW + " " + Token.TokenType.REFERENCE; + } + + public String description() { + return """ + Information about a reference. + """; + } + + @Override + public boolean matchesForHelp(List helpArgs) { + Node arg0 = helpArgs.get(0); + Node arg1 = helpArgs.size() >= 2 ? helpArgs.get(1) : null; + return arg0 instanceof Token + && arg0.getType() == Token.TokenType.SHOW + && (arg1 == null || (arg1 instanceof Token && arg1.getType() == Token.TokenType.REFERENCE)); + } +} diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/UseReferenceCommand.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/UseReferenceCommand.java new file mode 100644 index 00000000000..3dc92399f53 --- /dev/null +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/commands/UseReferenceCommand.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.commands; + +import jakarta.annotation.Nonnull; +import java.util.List; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.model.Reference; +import org.projectnessie.nessie.cli.cli.NessieCli; +import org.projectnessie.nessie.cli.cmdspec.UseReferenceCommandSpec; +import org.projectnessie.nessie.cli.grammar.Node; +import org.projectnessie.nessie.cli.grammar.Token; + +public class UseReferenceCommand extends NessieCommand { + public UseReferenceCommand() {} + + @Override + public void execute(@Nonnull NessieCli nessieCli, UseReferenceCommandSpec commandSpec) + throws Exception { + NessieApiV2 api = nessieCli.mandatoryNessieApi(); + + // TODO handle `commandSpec.getRefType()` + + Reference reference = api.getReference().refName(commandSpec.getRef()).get(); + + nessieCli.setCurrentReference(reference); + } + + public String name() { + return Token.TokenType.USE + " " + Token.TokenType.REFERENCE; + } + + public String description() { + return """ + Make the given reference the current reference. + """; + } + + @Override + public boolean matchesForHelp(List helpArgs) { + Node arg0 = helpArgs.get(0); + return arg0 instanceof Token && arg0.getType() == Token.TokenType.USE; + } +} diff --git a/cli/cli/src/main/resources/application.properties b/cli/cli/src/main/resources/application.properties new file mode 100644 index 00000000000..881627e5fc9 --- /dev/null +++ b/cli/cli/src/main/resources/application.properties @@ -0,0 +1,37 @@ +# +# Copyright (C) 2020 Dremio +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Quarkus settings +## Visit here for all configs: https://quarkus.io/guides/all-config +## some parameters are only configured at build time. These have been marked as such https://quarkus.io/guides/config#overriding-properties-at-runtime +quarkus.log.level=INFO +quarkus.log.console.level=WARN +#quarkus.log.min-level=ALL +# Somehow the trace-relevant IDs do not appear on the console, but they do in a log file... :( +#quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%X{traceId},%X{spanId},%X{sampled}] [%c{3.}] (%t) %s%e%n +quarkus.log.file.level=INFO +quarkus.log.file.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %h %N[%i] %-5p [%X{traceId},%X{spanId},%X{sampled}] [%c{3.}] (%t) %s%e%n +quarkus.log.category."io.netty".level=WARN + +# Do not print the banner, because that prints _after_ the CLI output :( +quarkus.banner.enabled=false + +# Quarkus build settings - only change if building/deploying locally + +## Quarkus required setting for third party indexing +# fixed at buildtime +quarkus.index-dependency.guava.group-id=com.google.guava +quarkus.index-dependency.guava.artifact-id=guava diff --git a/cli/cli/src/main/resources/org/projectnessie/nessie/cli/cli/banner.txt b/cli/cli/src/main/resources/org/projectnessie/nessie/cli/cli/banner.txt new file mode 100644 index 00000000000..e6c3173dbb4 --- /dev/null +++ b/cli/cli/src/main/resources/org/projectnessie/nessie/cli/cli/banner.txt @@ -0,0 +1,8 @@ + +███╗ ██╗███████╗███████╗███████╗██╗███████╗ ██████╗██╗ ██╗ +████╗ ██║██╔════╝██╔════╝██╔════╝██║██╔════╝ ██╔════╝██║ ██║ +██╔██╗ ██║█████╗ ███████╗███████╗██║█████╗ ██║ ██║ ██║ +██║╚██╗██║██╔══╝ ╚════██║╚════██║██║██╔══╝ ██║ ██║ ██║ +██║ ╚████║███████╗███████║███████║██║███████╗ ╚██████╗███████╗██║ +╚═╝ ╚═══╝╚══════╝╚══════╝╚══════╝╚═╝╚══════╝ ╚═════╝╚══════╝╚═╝ + by https://projectnessie.org diff --git a/cli/cli/src/main/resources/org/projectnessie/nessie/cli/cli/welcome.txt b/cli/cli/src/main/resources/org/projectnessie/nessie/cli/cli/welcome.txt new file mode 100644 index 00000000000..a526318c304 --- /dev/null +++ b/cli/cli/src/main/resources/org/projectnessie/nessie/cli/cli/welcome.txt @@ -0,0 +1,8 @@ + +Welcome to the Nessie CLI REPL! + +Use HELP to get information about available commands. +Commands can be autocompleted by pressing TAB. + +Tip: Ctrl-C cannot be used to exit the REPL or paged output. Use Ctrl-D instead. + diff --git a/cli/cli/src/test/java/org/projectnessie/nessie/cli/commands/TestLinesInputStream.java b/cli/cli/src/test/java/org/projectnessie/nessie/cli/commands/TestLinesInputStream.java new file mode 100644 index 00000000000..14e975714ca --- /dev/null +++ b/cli/cli/src/test/java/org/projectnessie/nessie/cli/commands/TestLinesInputStream.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.commands; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class TestLinesInputStream { + @ParameterizedTest + @MethodSource("sources") + public void readAllBytes(Supplier> sourceSupplier) throws IOException { + Stream source = sourceSupplier.get(); + + byte[] data = new LinesInputStream(source).readAllBytes(); + + String ref = sourceSupplier.get().map(s -> s + '\n').collect(Collectors.joining()); + + assertThat(new String(data, UTF_8)).isEqualTo(ref); + } + + @ParameterizedTest + @MethodSource("sources") + public void read(Supplier> sourceSupplier) throws IOException { + Stream source = sourceSupplier.get(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + try (LinesInputStream in = new LinesInputStream(source)) { + while (true) { + int rd = in.read(); + if (rd == -1) { + break; + } + out.write(rd); + } + } + + String ref = sourceSupplier.get().map(s -> s + '\n').collect(Collectors.joining()); + + assertThat(out.toString(UTF_8)).isEqualTo(ref); + } + + @ParameterizedTest + @MethodSource("sources") + public void read13(Supplier> sourceSupplier) throws IOException { + Stream source = sourceSupplier.get(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + byte[] buf = new byte[13]; + try (LinesInputStream in = new LinesInputStream(source)) { + + while (true) { + int rd = in.read(buf); + if (rd == -1) { + break; + } + out.write(buf, 0, rd); + } + } + + String ref = sourceSupplier.get().map(s -> s + '\n').collect(Collectors.joining()); + + assertThat(out.toString(UTF_8)).isEqualTo(ref); + } + + @ParameterizedTest + @MethodSource("sources") + public void read13mid(Supplier> sourceSupplier) throws IOException { + Stream source = sourceSupplier.get(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + byte[] buf = new byte[26]; + try (LinesInputStream in = new LinesInputStream(source)) { + + while (true) { + int rd = in.read(buf, 5, 5 + 13); + if (rd == -1) { + break; + } + out.write(buf, 5, rd); + } + } + + String ref = sourceSupplier.get().map(s -> s + '\n').collect(Collectors.joining()); + + assertThat(out.toString(UTF_8)).isEqualTo(ref); + } + + static Stream>> sources() { + return Stream.of( + () -> IntStream.range(0, 1000).mapToObj(i -> ""), + () -> IntStream.range(0, 1000).mapToObj(i -> "line #" + i), + () -> IntStream.range(0, 1000).mapToObj(i -> "line #" + i + " " + "1234567890".repeat(10))); + } +} diff --git a/cli/grammar/build.gradle.kts b/cli/grammar/build.gradle.kts new file mode 100644 index 00000000000..7227390eba1 --- /dev/null +++ b/cli/grammar/build.gradle.kts @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2022 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("nessie-conventions-quarkus") + alias(libs.plugins.jmh) +} + +val congocc by configurations.creating + +dependencies { + compileOnly(libs.jakarta.annotation.api) + compileOnly(libs.immutables.value.annotations) + annotationProcessor(libs.immutables.value.processor) + + implementation(libs.agrona) + implementation(libs.guava) + implementation(libs.slf4j.api) + + congocc(libs.congocc) + + testFixturesApi(platform(libs.junit.bom)) + testFixturesApi(libs.bundles.junit.testing) + testFixturesApi("org.antlr:antlr4:${libs.antlr.antlr4.runtime.get().version}") + testFixturesApi(libs.jakarta.annotation.api) + testFixturesApi(libs.immutables.value.annotations) + + testFixturesRuntimeOnly(libs.logback.classic) + + jmhImplementation(libs.jmh.core) + jmhAnnotationProcessor(libs.jmh.generator.annprocess) +} + +abstract class CongoCcGenerate : JavaExec() { + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val sourceDir: DirectoryProperty + + @get:OutputDirectory abstract val outputDir: DirectoryProperty +} + +val genGrammarDir = project.layout.buildDirectory.dir("generated/sources/congocc/main") + +val generateCcc = + tasks.register("generateCcc", CongoCcGenerate::class.java) { + sourceDir = projectDir.resolve("src/main/congocc") + outputDir = genGrammarDir + + classpath(congocc) + + doFirst { delete(genGrammarDir) } + + mainClass = "org.congocc.app.Main" + workingDir(projectDir) + argumentProviders.add( + CommandLineArgumentProvider { + val base = + listOf( + "-d", + genGrammarDir.get().asFile.toString(), + "-jdk17", + "-n", + sourceDir.get().file("nessie-cli-java.ccc").asFile.relativeTo(projectDir).toString() + ) + if (logger.isInfoEnabled) base else (base + listOf("-q")) + } + ) + } + +tasks.named("compileJava") { dependsOn(generateCcc) } + +sourceSets { main { java { srcDir(genGrammarDir) } } } + +tasks.withType().configureEach { + // Cannot exclude build/ as a "general configuration", because the Checstyle task creates an + // ant script behind the scenes, and that only supports "string" pattern matching using. + // The base directories are the source directories, so all patterns match against paths + // relative to a source-directory, not against full path names, not even relative to the current + // project. + exclude("org/projectnessie/nessie/cli/grammar/*") +} + +tasks.named("processJmhJandexIndex").configure { enabled = false } + +tasks.named("processTestJandexIndex").configure { enabled = false } + +jmh { jmhVersion = libs.versions.jmh.get() } + +tasks.named("jmhJar") { manifest { attributes["Multi-Release"] = "true" } } diff --git a/cli/grammar/src/jmh/java/org/projectnessie/nessie/cli/grammar/NessieCliParserBench.java b/cli/grammar/src/jmh/java/org/projectnessie/nessie/cli/grammar/NessieCliParserBench.java new file mode 100644 index 00000000000..d3dbffd3758 --- /dev/null +++ b/cli/grammar/src/jmh/java/org/projectnessie/nessie/cli/grammar/NessieCliParserBench.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.grammar; + +import static java.util.concurrent.TimeUnit.MICROSECONDS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@Warmup(iterations = 2, time = 2000, timeUnit = MILLISECONDS) +@Measurement(iterations = 3, time = 1000, timeUnit = MILLISECONDS) +@Fork(1) +@Threads(4) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(MICROSECONDS) +public class NessieCliParserBench { + + @State(Scope.Benchmark) + public static class Args { + @Param({ + "CREATE branch if not exists foo;", + "DROP TAG main;", + "CONNECT TO http://127.0.0.1:19120/api/v2", + "CONNECT TO http://127.0.0.1:19120/api/v2 USING a=b", + "CONNECT TO http://127.0.0.1:19120/api/v2 USING a=b AND c=d", + "CONNECT TO http://127.0.0.1:19120/api/v2 USING a=b AND c=d" + + " AND \"foo.bar.foo.bar.foo.bar.foo.bar.foo.bar\"=\"foo.bar.foo.bar.foo.bar.foo.bar.foo.bar\"" + + " AND \"foo.bar.foo.bar.foo.bar.foo.bar.foo.bar\"=\"foo.bar.foo.bar.foo.bar.foo.bar.foo.bar\"" + + " AND \"foo.bar.foo.bar.foo.bar.foo.bar.foo.bar\"=\"foo.bar.foo.bar.foo.bar.foo.bar.foo.bar\"" + + " AND \"foo.bar.foo.bar.foo.bar.foo.bar.foo.bar\"=\"foo.bar.foo.bar.foo.bar.foo.bar.foo.bar\"" + + " AND \"foo.bar.foo.bar.foo.bar.foo.bar.foo.bar\"=\"foo.bar.foo.bar.foo.bar.foo.bar.foo.bar\"" + + " AND \"foo.bar.foo.bar.foo.bar.foo.bar.foo.bar\"=\"foo.bar.foo.bar.foo.bar.foo.bar.foo.bar\"", + }) + String input; + } + + @Benchmark + public Node singleStatement(Args params) { + NessieCliLexer lexer = new NessieCliLexer(params.input); + NessieCliParser parser = new NessieCliParser(lexer); + parser.SingleStatement(); + return parser.rootNode(); + } +} diff --git a/cli/grammar/src/main/congocc/nessie-cli-java.ccc b/cli/grammar/src/main/congocc/nessie-cli-java.ccc new file mode 100644 index 00000000000..1ad7d548dd6 --- /dev/null +++ b/cli/grammar/src/main/congocc/nessie-cli-java.ccc @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +PARSER_PACKAGE="org.projectnessie.nessie.cli.grammar"; +PARSER_CLASS=NessieCliParser; +LEXER_CLASS=NessieCliLexer; +// GENERATE_ANNOTATIONS=true; // TODO doesn't work :( + +NODE_PACKAGE="org.projectnessie.nessie.cli.grammar.ast"; + +INCLUDE "nessie-cli.ccc" + +INJECT PARSER_CLASS : +import java.util.EnumMap; +import org.projectnessie.nessie.cli.cmdspec.*; +{ + private CompletionType completionType; + public CompletionType completionType() { + return completionType; + } +} + +INJECT Script : implements CommandContainer; +INJECT Script : +import java.util.List; +import org.projectnessie.nessie.cli.cmdspec.*; +{ + public List getCommandSpecs() { + return commandSpecs(); + } +} + +INJECT SingleStatement : implements CommandContainer; +INJECT SingleStatement : +import java.util.List; +import org.projectnessie.nessie.cli.cmdspec.*; +{ + public CommandSpec getCommandSpec() { + List specs = commandSpecs(); + return specs.isEmpty() ? null : specs.get(0); + } +} + +INJECT Ident : implements IdentifierOrLiteral; +INJECT Ident : +{ + @Override + public String getStringValue() { + return getSource(); + } +} + +INJECT StringLiteral : implements IdentifierOrLiteral; +INJECT StringLiteral : +{ + @Override + public String getStringValue() { + String src = getSource(); + // Remove quotes + return src.substring(1, src.length() - 1); + } +} + +INJECT UriLiteral : implements IdentifierOrLiteral; +INJECT UriLiteral : +{ + @Override + public String getStringValue() { + return getSource(); + } +} + +INJECT ConnectStatement : implements ConnectCommandSpec; +INJECT ConnectStatement : +import org.projectnessie.nessie.cli.cmdspec.*; +{ + @Override + public String getUri() { + return ((IdentifierOrLiteral)getNamedChild("uri")).getStringValue(); + } + + @Override + public String getInitialReference() { + return null; + } + + @Override + public Map getParameters() { + Map params = new HashMap<>(); + List children = children(); + for (int i = 0; i < children.size(); i++) { + Node child = children.get(i); + if (child.getType() == USING) { + for (i++; i < children.size(); ) { + Node key = children.get(i++); + if (children.get(i++).getType() != EQUAL) { + throw new IllegalArgumentException("Syntax error, missing '='"); + } + Node value = children.get(i++); + + params.put( + ((IdentifierOrLiteral)key).getStringValue(), + ((IdentifierOrLiteral)value).getStringValue() + ); + + if (children.size() == i || children.get(i++).getType() != AND) { + break; + } + } + break; + } + } + return params; + } +} + +INJECT CreateReferenceStatement : implements CreateReferenceCommandSpec; +INJECT CreateReferenceStatement : +import org.projectnessie.nessie.cli.cmdspec.*; +{ + @Override + public boolean isConditional() { + return getNamedChild("conditional")!=null; + } + + @Override + public String getRef() { + return ((IdentifierOrLiteral)getNamedChild("ref")).getStringValue(); + } + + @Override + public String getRefType() { + return getNamedChild("type").getSource(); + } +} + +INJECT DropReferenceStatement : implements DropReferenceCommandSpec; +INJECT DropReferenceStatement : +import org.projectnessie.nessie.cli.cmdspec.*; +INJECT DropReferenceStatement : +{ + @Override + public boolean isConditional() { + return getNamedChild("conditional")!=null; + } + + @Override + public String getRef() { + return ((IdentifierOrLiteral)getNamedChild("ref")).getStringValue(); + } + + @Override + public String getRefType() { + return getNamedChild("type").getSource(); + } +} + +INJECT AssignReferenceStatement : implements AssignReferenceCommandSpec; +INJECT AssignReferenceStatement : +import org.projectnessie.nessie.cli.cmdspec.*; +INJECT AssignReferenceStatement : +{ + @Override + public String getRef() { + return ((IdentifierOrLiteral)getNamedChild("ref")).getStringValue(); + } + + @Override + public String getRefType() { + return getNamedChild("type").getSource(); + } + + @Override + public String getTo() { + IdentifierOrLiteral n = (IdentifierOrLiteral) getNamedChild("to"); + return n != null ? n.getStringValue() : null; + } + + @Override + public String getToTimestampOrHash() { + IdentifierOrLiteral n = (IdentifierOrLiteral) getNamedChild("toTimestampOrHash"); + return n != null ? n.getStringValue() : null; + } +} + +INJECT UseReferenceStatement : implements UseReferenceCommandSpec; +INJECT UseReferenceStatement : +import org.projectnessie.nessie.cli.cmdspec.*; +INJECT UseReferenceStatement : +{ + @Override + public String getRefType() { + Node type = getNamedChild("type"); + return type != null ? type.getSource() : null; + } + + @Override + public String getRef() { + return ((IdentifierOrLiteral)getNamedChild("ref")).getStringValue(); + } +} + +INJECT ListReferencesStatement : implements ListReferencesCommandSpec; +INJECT ListReferencesStatement : +import org.projectnessie.nessie.cli.cmdspec.*; + +INJECT ShowReferenceStatement : implements ShowReferenceCommandSpec; +INJECT ShowReferenceStatement : +import org.projectnessie.nessie.cli.cmdspec.*; +{ + @Override + public String getRef() { + return ((IdentifierOrLiteral)getNamedChild("ref")).getStringValue(); + } +} + +INJECT MergeBranchStatement : implements MergeBranchCommandSpec; +INJECT MergeBranchStatement : +import org.projectnessie.nessie.cli.cmdspec.*; +{ + @Override + public String getRef() { + return ((IdentifierOrLiteral)getNamedChild("ref")).getStringValue(); + } + + @Override + public String getRefTimestampOrHash() { + IdentifierOrLiteral n = (IdentifierOrLiteral) getNamedChild("refTimestampOrHash"); + return n != null ? n.getStringValue() : null; + } + + @Override + public String getInto() { + IdentifierOrLiteral n = (IdentifierOrLiteral) getNamedChild("into"); + return n != null ? n.getStringValue() : null; + } +} + +INJECT ShowLogStatement : implements ShowLogCommandSpec; +INJECT ShowLogStatement : +import org.projectnessie.nessie.cli.cmdspec.*; +{ + @Override + public String getRef() { + return ((IdentifierOrLiteral)getNamedChild("ref")).getStringValue(); + } +} + +INJECT ExitStatement : implements ExitCommandSpec; +INJECT ExitStatement : +import org.projectnessie.nessie.cli.cmdspec.*; + +INJECT HelpStatement : implements HelpCommandSpec; +INJECT HelpStatement : +import org.projectnessie.nessie.cli.cmdspec.*; +{ + @Override + public List getArguments() { + List ch = children(); + return ch.subList(1, ch.size()); + } +} + +INJECT ParseException : +{ + public Set getExpectedTokenTypes() { + // expectedTypes is 'Set expectedTypes', but it's all TokenType, so... + Set x = expectedTypes; + return x; + } +} diff --git a/cli/grammar/src/main/congocc/nessie-cli-lexer.ccc b/cli/grammar/src/main/congocc/nessie-cli-lexer.ccc new file mode 100644 index 00000000000..2a2b69a7bbd --- /dev/null +++ b/cli/grammar/src/main/congocc/nessie-cli-lexer.ccc @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +SKIP : #Whitespace; + +// Note: tokens should be ordered by their lenth (see https://parsers.org/) + +TOKEN #Command + : + | + | + | + | + | + | + | + | + | + ; + +TOKEN + : #Semicolon + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + ; + +TOKEN #Literal + : #BooleanLiteral + | #BooleanLiteral + | <#REGULAR_CHAR : ~["\u0000"-"\u001F",'"',"\\"]> + | #StringLiteral + | #Ident + | #UriLiteral + ; + +TOKEN #ReferenceTypes + : #Branch + | #Tag + ; + +UNPARSED : #SingleLineComment ; +UNPARSED : < ?MULTI_LINE_COMMENT : "/*" (~[])* "*/" > #MultiLineComment ; diff --git a/cli/grammar/src/main/congocc/nessie-cli.ccc b/cli/grammar/src/main/congocc/nessie-cli.ccc new file mode 100644 index 00000000000..82c40bf5497 --- /dev/null +++ b/cli/grammar/src/main/congocc/nessie-cli.ccc @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//DEFAULT_LEXICAL_STATE=Script; +//TAB_SIZE=4; +//TABS_TO_SPACES=4; +//PRESERVE_TABS; +IGNORE_CASE; +//TREE_BUILDING_ENABLED=true; +//LOOKAHEAD=4; // TODO ?? +//FAULT_TOLERANT; // TODO ?? +//FAULT_TOLERANT_DEFAULT=false; + +// see https://parsers.org/javacc21/nested-lookahead-redux/ +LEGACY_GLITCHY_LOOKAHEAD=false; + +DEACTIVATE_TOKENS=IDENTIFIER,STRING_LITERAL,URI; + +INCLUDE "nessie-cli-lexer.ccc" + +Script : + Statement! + ( Statement =>||)*! + ()? + + ; + +SingleStatement : + Statement + ()? + + ; + +Statement + : ConnectStatement + | CreateReferenceStatement + | DropReferenceStatement + | AssignReferenceStatement + | UseReferenceStatement + | ListReferencesStatement + | ShowStatement + | MergeBranchStatement + | ExitStatement + | HelpStatement + ; + +HelpStatement + : + ( + + | + | ( | )? + | ( | )? + | ()? + | ( | )? + | ( | )? + | + | + | + )? + ; + +ExitStatement + : + ; + +ConnectStatement + : + /uri/=Uri + ( + ParamKey ParamValue + ( + + ParamKey ParamValue + )* + )? + ; + +CreateReferenceStatement + : + /type/=ReferenceType + { completionType=CompletionType.NON_EXISTING_REFERENCE_NAME; } + ( (/conditional/= )? /ref/=ReferenceName + | /ref/=ReferenceName + ) + ( { completionType=CompletionType.REFERENCE_SPEC; } /from/=ReferenceSpec)? + ; + +AssignReferenceStatement + : + /type/=ReferenceType + { completionType=CompletionType.REFERENCE_NAME; } + [/ref/=ExistingReference] + [ /to/=ExistingReference + [ /toTimestampOrHash/=TimestampOrHash] + ] + ; + +DropReferenceStatement + : + /type/=ReferenceType + { completionType=CompletionType.REFERENCE_NAME; } + ( (/conditional/= )? /ref/=ExistingReference + | /ref/=ExistingReference + ) + ; + +UseReferenceStatement + : + { completionType=CompletionType.REFERENCE_NAME; } + ( /type/=ReferenceType /ref/=ExistingReference + | /ref/=ExistingReference + ) + [ /at/=TimestampOrHash] + ; + +ShowStatement + : + ( + ShowLogStatement + | + ShowReferenceStatement + ) + ; + +ShowLogStatement + : + ( { completionType=CompletionType.REFERENCE_NAME; } + [/ref/=ExistingReference] )? + ; + +ShowReferenceStatement + : + ( { completionType=CompletionType.REFERENCE_NAME; } + [/ref/=ExistingReference] )? + ; + +ListReferencesStatement + : + ( /startsWith/=Identifier)? + ( /contains/=Identifier)? + ; + +MergeBranchStatement + : + (/type/=ReferenceType)? + { completionType=CompletionType.REFERENCE_NAME; } + /ref/=ExistingReference + [ /refTimestampOrHash/=TimestampOrHash] + [ /into/=ExistingReference] + ; + +ReferenceType + : + | + ; + + +ReferenceSpec + // TODO define - can be one of: + // - a reference name + // - a reference name with a hash + // - a hash + // - a reference name with timestamp + // - a timestamp (assuming the "current" named reference) + : + ; + +TimestampOrHash + // TODO define + : + ; + +ReferenceName + : ACTIVATE_TOKENS IDENTIFIER, STRING_LITERAL (Identifier) =>||; + +ExistingReference + : ACTIVATE_TOKENS IDENTIFIER, STRING_LITERAL (Identifier) =>||; + +Uri + : ACTIVATE_TOKENS URI, STRING_LITERAL (UriIdentifier) =>||; + +ParamKey + : ACTIVATE_TOKENS IDENTIFIER, STRING_LITERAL (Identifier) =>||; + +ParamValue + : ACTIVATE_TOKENS IDENTIFIER, STRING_LITERAL (Identifier) =>||; + +UriIdentifier + : + | + ; + +Identifier + : + | + ; diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/AssignReferenceCommandSpec.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/AssignReferenceCommandSpec.java new file mode 100644 index 00000000000..467eb6b7355 --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/AssignReferenceCommandSpec.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cmdspec; + +import org.immutables.value.Value; + +@Value.Immutable +@SuppressWarnings("immutables:subtype") +public interface AssignReferenceCommandSpec extends RefWithTypeCommandSpec { + default CommandType commandType() { + return CommandType.ASSIGN_REFERENCE; + } + + String getTo(); + + String getToTimestampOrHash(); +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/CommandContainer.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/CommandContainer.java new file mode 100644 index 00000000000..70dfb6dd952 --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/CommandContainer.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cmdspec; + +import static org.projectnessie.nessie.cli.grammar.Token.TokenType.LOG; +import static org.projectnessie.nessie.cli.grammar.Token.TokenType.REFERENCE; + +import java.util.ArrayList; +import java.util.List; +import org.projectnessie.nessie.cli.grammar.Node; +import org.projectnessie.nessie.cli.grammar.Token; +import org.projectnessie.nessie.cli.grammar.ast.Keyword; +import org.projectnessie.nessie.cli.grammar.ast.ShowStatement; + +public interface CommandContainer extends Node { + default List commandSpecs() { + List commandSpecs = new ArrayList<>(); + for (Node child : children()) { + + if (child instanceof CommandSpec) { + // Have a "proper" CommandSpec instance, use it. + commandSpecs.add((CommandSpec) child); + continue; + } + + // Handle commands that may have no arguments. Commands w/o arguments (e.g. "SHOW LOG;") do + // *not* produce a ShowLogCommandSpec, so we have to check the tokens. + if (child instanceof ShowStatement) { + CommandSpec spec = child.firstChildOfType(CommandSpec.class); + if (spec == null) { + Keyword kw = child.firstChildOfType(Keyword.class); + spec = + switch (kw.getType()) { + case LOG -> ImmutableShowLogCommandSpec.builder().sourceNode(child).build(); + case REFERENCE -> + ImmutableShowReferenceCommandSpec.builder().sourceNode(child).build(); + default -> null; + }; + } + if (spec != null) { + commandSpecs.add(spec); + } + continue; + } + + // Leaves us with the "simple no arg" commands HELP and EXIT + if (child instanceof Token t) { + CommandSpec spec = + switch (t.getType()) { + case EXIT -> ImmutableExitCommandSpec.builder().sourceNode(child).build(); + case HELP -> ImmutableHelpCommandSpec.builder().sourceNode(child).build(); + case LIST -> ImmutableListReferencesCommandSpec.builder().sourceNode(child).build(); + default -> null; + }; + if (spec != null) { + commandSpecs.add(spec); + } + // continue; + } + } + return commandSpecs; + } +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/CommandSpec.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/CommandSpec.java new file mode 100644 index 00000000000..0a384a301c0 --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/CommandSpec.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cmdspec; + +import jakarta.annotation.Nullable; +import org.immutables.value.Value; +import org.projectnessie.nessie.cli.grammar.Node; + +public interface CommandSpec { + CommandType commandType(); + + @Value.Default + @Nullable + default Node sourceNode() { + if (this instanceof Node) { + return (Node) this; + } + return null; + } +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/CommandType.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/CommandType.java new file mode 100644 index 00000000000..4b99ed8f637 --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/CommandType.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cmdspec; + +public enum CommandType { + CONNECT, + CREATE_REFERENCE, + DROP_REFERENCE, + ASSIGN_REFERENCE, + EXIT, + HELP, + LIST_REFERENCES, + MERGE_BRANCH, + SHOW_LOG, + SHOW_REFERENCE, + USE_REFERENCE, +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ConnectCommandSpec.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ConnectCommandSpec.java new file mode 100644 index 00000000000..ac28b4eef25 --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ConnectCommandSpec.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cmdspec; + +import jakarta.annotation.Nullable; +import java.util.Map; +import org.immutables.value.Value; + +@Value.Immutable +@SuppressWarnings("immutables:subtype") +public interface ConnectCommandSpec extends CommandSpec { + default CommandType commandType() { + return CommandType.CONNECT; + } + + String getUri(); + + @Nullable + String getInitialReference(); + + Map getParameters(); +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/CreateReferenceCommandSpec.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/CreateReferenceCommandSpec.java new file mode 100644 index 00000000000..38fc4c8458f --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/CreateReferenceCommandSpec.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cmdspec; + +import org.immutables.value.Value; + +@Value.Immutable +@SuppressWarnings("immutables:subtype") +public interface CreateReferenceCommandSpec extends RefWithTypeCommandSpec { + default CommandType commandType() { + return CommandType.CREATE_REFERENCE; + } + + boolean isConditional(); +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/DropReferenceCommandSpec.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/DropReferenceCommandSpec.java new file mode 100644 index 00000000000..a343f465e29 --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/DropReferenceCommandSpec.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cmdspec; + +import org.immutables.value.Value; + +@Value.Immutable +@SuppressWarnings("immutables:subtype") +public interface DropReferenceCommandSpec extends RefWithTypeCommandSpec { + default CommandType commandType() { + return CommandType.DROP_REFERENCE; + } + + boolean isConditional(); +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ExitCommandSpec.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ExitCommandSpec.java new file mode 100644 index 00000000000..ee50465aa32 --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ExitCommandSpec.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cmdspec; + +import org.immutables.value.Value; + +@Value.Immutable +public interface ExitCommandSpec extends CommandSpec { + default CommandType commandType() { + return CommandType.EXIT; + } +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/HelpCommandSpec.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/HelpCommandSpec.java new file mode 100644 index 00000000000..ff9f15b3581 --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/HelpCommandSpec.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cmdspec; + +import java.util.List; +import org.immutables.value.Value; +import org.projectnessie.nessie.cli.grammar.Node; + +@Value.Immutable +public interface HelpCommandSpec extends CommandSpec { + default CommandType commandType() { + return CommandType.HELP; + } + + List getArguments(); +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ListReferencesCommandSpec.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ListReferencesCommandSpec.java new file mode 100644 index 00000000000..1e635a3479b --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ListReferencesCommandSpec.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cmdspec; + +import org.immutables.value.Value; + +@Value.Immutable +public interface ListReferencesCommandSpec extends CommandSpec { + default CommandType commandType() { + return CommandType.LIST_REFERENCES; + } +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/MergeBranchCommandSpec.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/MergeBranchCommandSpec.java new file mode 100644 index 00000000000..98cd3929a6b --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/MergeBranchCommandSpec.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cmdspec; + +import org.immutables.value.Value; + +@Value.Immutable +public interface MergeBranchCommandSpec extends RefCommandSpec { + default CommandType commandType() { + return CommandType.MERGE_BRANCH; + } + + String getRefTimestampOrHash(); + + String getInto(); +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/RefCommandSpec.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/RefCommandSpec.java new file mode 100644 index 00000000000..584db8bbb9d --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/RefCommandSpec.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cmdspec; + +public interface RefCommandSpec extends CommandSpec { + + /** The reference name exactly as it appears in the source statement. */ + String getRef(); +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/RefWithTypeCommandSpec.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/RefWithTypeCommandSpec.java new file mode 100644 index 00000000000..ca96541f994 --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/RefWithTypeCommandSpec.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cmdspec; + +public interface RefWithTypeCommandSpec extends RefCommandSpec { + + /** The reference type exactly as it appears in the source statement. */ + String getRefType(); +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ShowLogCommandSpec.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ShowLogCommandSpec.java new file mode 100644 index 00000000000..e70761f44ce --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ShowLogCommandSpec.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cmdspec; + +import jakarta.annotation.Nullable; +import org.immutables.value.Value; + +@Value.Immutable +public interface ShowLogCommandSpec extends RefCommandSpec { + default CommandType commandType() { + return CommandType.SHOW_LOG; + } + + @Nullable + @Override + String getRef(); +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ShowReferenceCommandSpec.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ShowReferenceCommandSpec.java new file mode 100644 index 00000000000..054613a1b20 --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/ShowReferenceCommandSpec.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cmdspec; + +import jakarta.annotation.Nullable; +import org.immutables.value.Value; + +@Value.Immutable +public interface ShowReferenceCommandSpec extends RefCommandSpec { + default CommandType commandType() { + return CommandType.SHOW_REFERENCE; + } + + @Nullable + @Override + String getRef(); +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/UseReferenceCommandSpec.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/UseReferenceCommandSpec.java new file mode 100644 index 00000000000..6155bb59f84 --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/UseReferenceCommandSpec.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.cmdspec; + +import jakarta.annotation.Nullable; +import org.immutables.value.Value; + +@Value.Immutable +public interface UseReferenceCommandSpec extends RefCommandSpec { + default CommandType commandType() { + return CommandType.USE_REFERENCE; + } + + @Nullable + String getRefType(); +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/package-info.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/package-info.java new file mode 100644 index 00000000000..23e54f598e8 --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/cmdspec/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@Value.Style(depluralize = true, allParameters = true) +package org.projectnessie.nessie.cli.cmdspec; + +import org.immutables.value.Value; diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/completer/CliCompleter.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/completer/CliCompleter.java new file mode 100644 index 00000000000..2da521bcb98 --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/completer/CliCompleter.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.completer; + +import static java.util.Locale.ROOT; + +import java.util.function.Consumer; +import org.projectnessie.nessie.cli.grammar.CompletionType; +import org.projectnessie.nessie.cli.grammar.NessieCliLexer; +import org.projectnessie.nessie.cli.grammar.NessieCliParser; +import org.projectnessie.nessie.cli.grammar.ParseException; +import org.projectnessie.nessie.cli.grammar.Token; + +public abstract class CliCompleter { + private final String source; + + private final NessieCliParser parser; + private final Consumer producer; + + public CliCompleter(String input, int cursor, Consumer producer) { + this.source = input.substring(0, cursor); + + NessieCliLexer lexer = new NessieCliLexer(source); + this.parser = new NessieCliParser(lexer); + + this.producer = producer; + } + + /** + * Runs a completion attempt. + * + *

Provides completions for tokens via {@link #tokenCandidateStartsWith(String, String)} et al + * and calls {@link #completeWithLiteral(CompletionType, String, String, boolean)} for literals. + * + * @return {@code true} if the input could not be parsed, {@code false} if the input could be + * successfully parsed. + */ + public boolean tryStatement() { + try { + producer.accept(parser); + + // TODO auto complete for "HELP ..." + "SHOW LOG ..." + "SHOW REFERENCE ..." + // these commands are valid w/o any other options, but have other options, that can be + // auto-completed + + // Input was successfully parsed. If the last token is an identifier, try to complete it. + + if (parser.getToken(0).getType().isEOF()) { + Token prev = parser.getToken(-1); + switch (prev.getType()) { + case IDENTIFIER, STRING_LITERAL -> + completeWithIdentifier( + source.substring(0, prev.getEndOffset()), + source.substring(prev.getBeginOffset())); + + default -> { + // cannot have "unfinished" keywords for a successfully parsed statement + } + } + } + + return false; + } catch (ParseException e) { + int index = 0; + Token currentToken = parser.getToken(index); + + // Go to the token on which the cursor is. + int cursor = source.length(); + while (true) { + if (currentToken.getBeginOffset() <= cursor && cursor <= currentToken.getEndOffset()) { + break; + } + if (cursor < currentToken.getEndOffset()) { + Token prev = parser.getToken(--index); + if (prev == null) { + break; + } + currentToken = prev; + } + if (cursor >= currentToken.getEndOffset()) { + Token next = parser.getToken(++index); + if (next == null || next.getType().isEOF()) { + break; + } + currentToken = next; + } + } + + // Go back to the first invalid token. This finds the start of incomplete string literals, + // for example for an input like 'CREATE BRANCH "foo bar', it sets `currentToken` to the + // invalid token '"foo'. Have to skip over all non-INVALID tokens. + for (int i = index; ; i--) { + Token c = parser.getToken(i); + if (c == null) { + break; + } else if (c.getType().isInvalid()) { + currentToken = c; + } + } + + String trimmed; + if (currentToken.isInvalid()) { + trimmed = source.substring(0, currentToken.getBeginOffset()); + } else { + trimmed = source; + if (!trimmed.isEmpty() && !Character.isWhitespace(trimmed.charAt(trimmed.length() - 1))) { + trimmed += ' '; + } + } + + String currentSource = source.substring(currentToken.getBeginOffset(), cursor); + String currentUpper = currentSource.toUpperCase(ROOT); + boolean currentIsEmpty = currentSource.trim().isEmpty(); + + boolean handledIdentifier = false; + for (Token.TokenType expected : e.getExpectedTokenTypes()) { + switch (expected) { + case EOF, DUMMY, WHITESPACE -> {} + + case IDENTIFIER, STRING_LITERAL -> { + // Don't call 'completeWithIdentifier()' twice for the same input. + if (!handledIdentifier) { + completeWithIdentifier(trimmed, currentSource); + handledIdentifier = true; + } + } + + default -> { + // Replace the underscore with a space for "multi-word" tokens. + String tokenString = expected.name(); + String tokenUpper = tokenString.toUpperCase(ROOT); + + if (currentIsEmpty) { + tokenCandidateOther(trimmed, tokenString); + } else if (tokenUpper.startsWith(currentUpper)) { + tokenCandidateStartsWith(trimmed, tokenString); + } else if (tokenUpper.contains(currentUpper)) { + tokenCandidateContains(trimmed, tokenString); + } else { + tokenCandidateOther(trimmed, tokenString); + } + } + } + } + return true; + } + } + + private void completeWithIdentifier(String preceding, String toComplete) { + boolean quoted = false; + if (toComplete.startsWith("\"")) { + // in STRING_LITERAL + quoted = true; + toComplete = toComplete.substring(1); + if (toComplete.endsWith("\"")) { + toComplete = toComplete.substring(0, toComplete.length() - 1); + } + } + + completeWithLiteral(parser.completionType(), preceding, toComplete, quoted); + } + + protected abstract void completeWithLiteral( + CompletionType completionType, String preceding, String toComplete, boolean quoted); + + protected abstract void tokenCandidateStartsWith(String preceding, String toComplete); + + protected abstract void tokenCandidateContains(String preceding, String toComplete); + + protected abstract void tokenCandidateOther(String preceding, String toComplete); +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/grammar/CompletionType.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/grammar/CompletionType.java new file mode 100644 index 00000000000..25740b915cc --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/grammar/CompletionType.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.grammar; + +public enum CompletionType { + NON_EXISTING_REFERENCE_NAME, + REFERENCE_NAME, + REFERENCE_SPEC +} diff --git a/cli/grammar/src/main/java/org/projectnessie/nessie/cli/grammar/IdentifierOrLiteral.java b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/grammar/IdentifierOrLiteral.java new file mode 100644 index 00000000000..a53e6f9e1ec --- /dev/null +++ b/cli/grammar/src/main/java/org/projectnessie/nessie/cli/grammar/IdentifierOrLiteral.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.grammar; + +public interface IdentifierOrLiteral { + String getStringValue(); +} diff --git a/cli/grammar/src/test/java/org/projectnessie/nessie/cli/completer/TestCliCompleter.java b/cli/grammar/src/test/java/org/projectnessie/nessie/cli/completer/TestCliCompleter.java new file mode 100644 index 00000000000..fc627f3e08d --- /dev/null +++ b/cli/grammar/src/test/java/org/projectnessie/nessie/cli/completer/TestCliCompleter.java @@ -0,0 +1,569 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.nessie.cli.completer; + +import static org.assertj.core.api.Assertions.tuple; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; +import org.assertj.core.api.SoftAssertions; +import org.assertj.core.api.junit.jupiter.InjectSoftAssertions; +import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.projectnessie.nessie.cli.cmdspec.AssignReferenceCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.CommandSpec; +import org.projectnessie.nessie.cli.cmdspec.ConnectCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.CreateReferenceCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.DropReferenceCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.HelpCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.ImmutableAssignReferenceCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.ImmutableConnectCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.ImmutableCreateReferenceCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.ImmutableDropReferenceCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.ImmutableExitCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.ImmutableHelpCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.ImmutableListReferencesCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.ImmutableShowLogCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.ImmutableShowReferenceCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.ImmutableUseReferenceCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.ListReferencesCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.ShowLogCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.ShowReferenceCommandSpec; +import org.projectnessie.nessie.cli.cmdspec.UseReferenceCommandSpec; +import org.projectnessie.nessie.cli.grammar.CompletionType; +import org.projectnessie.nessie.cli.grammar.NessieCliLexer; +import org.projectnessie.nessie.cli.grammar.NessieCliParser; +import org.projectnessie.nessie.cli.grammar.Node; +import org.projectnessie.nessie.cli.grammar.ast.Script; +import org.projectnessie.nessie.cli.grammar.ast.SingleStatement; + +@ExtendWith(SoftAssertionsExtension.class) +public class TestCliCompleter { + @InjectSoftAssertions protected SoftAssertions soft; + + @ParameterizedTest + @MethodSource + public void literalCompletions( + String input, + boolean expectedOutcome, + String expected, + boolean expectedQuoted, + CompletionType expectedCompletionType) { + CliCompleter completer = + new CliCompleter(input, input.length(), NessieCliParser::SingleStatement) { + @Override + protected void completeWithLiteral( + CompletionType completionType, String preceding, String toComplete, boolean quoted) { + soft.assertThat(tuple(completionType, toComplete, quoted)) + .isEqualTo(tuple(expectedCompletionType, expected, expectedQuoted)); + } + + @Override + protected void tokenCandidateStartsWith(String preceding, String toComplete) { + // ignore + } + + @Override + protected void tokenCandidateContains(String preceding, String toComplete) { + // ignore + } + + @Override + protected void tokenCandidateOther(String preceding, String toComplete) { + // ignore + } + }; + + boolean outcome = completer.tryStatement(); + soft.assertThat(outcome).isEqualTo(expectedOutcome); + } + + static Stream literalCompletions() { + return Stream.of( + arguments("show reference f", false, "f", false, CompletionType.REFERENCE_NAME), + arguments("show log f", false, "f", false, CompletionType.REFERENCE_NAME), + arguments("create branch f", false, "f", false, CompletionType.NON_EXISTING_REFERENCE_NAME), + arguments("create branch \"f", true, "f", true, CompletionType.NON_EXISTING_REFERENCE_NAME), + arguments( + "create branch \"f\"", false, "f", true, CompletionType.NON_EXISTING_REFERENCE_NAME), + arguments( + "create branch \"foo non_keyword", + true, + "foo non_keyword", + true, + CompletionType.NON_EXISTING_REFERENCE_NAME), + arguments( + "create branch \"foo if not exists", + true, + "foo if not exists", + true, + CompletionType.NON_EXISTING_REFERENCE_NAME)); + } + + @ParameterizedTest + @MethodSource + public void completer( + String input, + boolean expectedOutcome, + List expectedStartsWith, + List expectedContains, + List expectedOther, + CompletionType expectedCompletionType) { + List startsWith = new ArrayList<>(); + List contains = new ArrayList<>(); + List other = new ArrayList<>(); + CliCompleter completer = + new CliCompleter(input, input.length(), NessieCliParser::SingleStatement) { + @Override + protected void completeWithLiteral( + CompletionType completionType, String preceding, String toComplete, boolean quoted) { + soft.assertThat(completionType).isEqualTo(expectedCompletionType); + // ignore literal completions in this test + } + + @Override + protected void tokenCandidateStartsWith(String preceding, String toComplete) { + startsWith.add(preceding + toComplete); + } + + @Override + protected void tokenCandidateContains(String preceding, String toComplete) { + contains.add(preceding + toComplete); + } + + @Override + protected void tokenCandidateOther(String preceding, String toComplete) { + other.add(preceding + toComplete); + } + }; + + boolean outcome = completer.tryStatement(); + soft.assertThat(outcome).isEqualTo(expectedOutcome); + + soft.assertThat(startsWith).containsExactlyInAnyOrderElementsOf(expectedStartsWith); + soft.assertThat(contains).containsExactlyInAnyOrderElementsOf(expectedContains); + soft.assertThat(other).containsExactlyInAnyOrderElementsOf(expectedOther); + } + + static Stream completer() { + return Stream.of( + arguments( + "h", + true, + List.of("HELP"), + List.of("SHOW"), + List.of("CONNECT", "USE", "DROP", "LIST", "EXIT", "MERGE", "ASSIGN", "CREATE"), + null), + // arguments( + // "help ", + // false, // statement is valid! ('HELP' without a statement) + // List.of(), + // List.of(), + // List.of( + // "help USE", + // "help DROP", + // "help LIST", + // "help SHOW", + // "help EXIT", + // "help MERGE", + // "help ASSIGN", + // "help CREATE"), + // null), + // arguments( + // "help u", + // true, + // List.of("help USE"), + // List.of(), + // List.of( + // "help DROP", + // "help LIST", + // "help SHOW", + // "help EXIT", + // "help MERGE", + // "help ASSIGN", + // "help CREATE"), + // null), + arguments( + "show", + true, + List.of(), + List.of(), + List.of("show LOG", "show REFERENCE"), + CompletionType.NON_EXISTING_REFERENCE_NAME), + arguments( + "show ", + true, + List.of(), + List.of(), + List.of("show LOG", "show REFERENCE"), + CompletionType.NON_EXISTING_REFERENCE_NAME), + arguments( + "show l", + true, + List.of("show LOG"), + List.of(), + List.of("show REFERENCE"), + CompletionType.REFERENCE_NAME), + arguments( + "show r", + true, + List.of("show REFERENCE"), + List.of(), + List.of("show LOG"), + CompletionType.REFERENCE_NAME), + arguments( + "create branch f", + false, + List.of(), + List.of(), + List.of(), + CompletionType.NON_EXISTING_REFERENCE_NAME), + arguments( + "create branch \"f", + true, + List.of(), + List.of(), + List.of("create branch IF"), + CompletionType.NON_EXISTING_REFERENCE_NAME), + arguments( + "create branch \"f\"", + false, + List.of(), + List.of(), + List.of(), + CompletionType.NON_EXISTING_REFERENCE_NAME), + arguments( + "create branch \"foo non_keyword", + true, + List.of(), + List.of(), + List.of("create branch IF"), + CompletionType.NON_EXISTING_REFERENCE_NAME), + arguments( + "create branch \"foo if not exists", + true, + List.of(), + List.of(), + List.of("create branch IF"), + CompletionType.NON_EXISTING_REFERENCE_NAME), + arguments( + "", + true, + List.of(), + List.of(), + List.of( + "CONNECT", "USE", "DROP", "LIST", "SHOW", "HELP", "EXIT", "MERGE", "ASSIGN", + "CREATE"), + null), + arguments( + " ", + true, + List.of(), + List.of(), + List.of( + " CONNECT", + " USE", + " DROP", + " LIST", + " SHOW", + " HELP", + " EXIT", + " MERGE", + " ASSIGN", + " CREATE"), + null), + arguments( + "C", + true, + List.of("CONNECT", "CREATE"), + List.of(), + List.of("USE", "DROP", "LIST", "SHOW", "HELP", "EXIT", "MERGE", "ASSIGN"), + null), + arguments( + "c", + true, + List.of("CONNECT", "CREATE"), + List.of(), + List.of("USE", "DROP", "LIST", "SHOW", "HELP", "EXIT", "MERGE", "ASSIGN"), + null), + arguments( + " C", + true, + List.of(" CONNECT", " CREATE"), + List.of(), + List.of(" USE", " DROP", " LIST", " SHOW", " HELP", " EXIT", " MERGE", " ASSIGN"), + null), + arguments( + " c", + true, + List.of(" CONNECT", " CREATE"), + List.of(), + List.of(" USE", " DROP", " LIST", " SHOW", " HELP", " EXIT", " MERGE", " ASSIGN"), + null), + arguments( + "X", + true, + List.of(), + List.of("EXIT"), + List.of("CONNECT", "USE", "DROP", "LIST", "SHOW", "HELP", "MERGE", "ASSIGN", "CREATE"), + null), + arguments( + "CREATE", true, List.of(), List.of(), List.of("CREATE BRANCH", "CREATE TAG"), null), + arguments( + "CREATE ", true, List.of(), List.of(), List.of("CREATE BRANCH", "CREATE TAG"), null), + arguments( + "create ", true, List.of(), List.of(), List.of("create BRANCH", "create TAG"), null), + arguments( + "cReAtE ", true, List.of(), List.of(), List.of("cReAtE BRANCH", "cReAtE TAG"), null), + arguments( + "CREATE B", true, List.of("CREATE BRANCH"), List.of(), List.of("CREATE TAG"), null), + arguments( + "create b", true, List.of("create BRANCH"), List.of(), List.of("create TAG"), null), + arguments( + "CREATE X", true, List.of(), List.of(), List.of("CREATE BRANCH", "CREATE TAG"), null), + arguments( + "CREATE R", true, List.of(), List.of("CREATE BRANCH"), List.of("CREATE TAG"), null), + arguments( + "CREATE A", true, List.of(), List.of("CREATE BRANCH", "CREATE TAG"), List.of(), null), + arguments( + "CREATE BRANCH", + true, + List.of(), + List.of(), + List.of("CREATE BRANCH IF"), + CompletionType.NON_EXISTING_REFERENCE_NAME), + arguments( + "CREATE BRANCH ", + true, + List.of(), + List.of(), + List.of("CREATE BRANCH IF"), + CompletionType.NON_EXISTING_REFERENCE_NAME), + arguments( + "DROP BRANCH", + true, + List.of(), + List.of(), + List.of("DROP BRANCH IF"), + CompletionType.REFERENCE_NAME), + arguments( + "DROP BRANCH ", + true, + List.of(), + List.of(), + List.of("DROP BRANCH IF"), + CompletionType.REFERENCE_NAME), + arguments( + "DROP BRANCH IF", + true, + List.of(), + List.of(), + List.of("DROP BRANCH IF EXISTS"), + CompletionType.REFERENCE_NAME), + arguments( + "DROP BRANCH IF ", + true, + List.of(), + List.of(), + List.of("DROP BRANCH IF EXISTS"), + CompletionType.REFERENCE_NAME), + arguments( + "USE\n BRANCH\n main\n AT ", + true, + List.of(), + List.of(), + List.of(), + // Must not reset the completion type in the parser, so this value is expected here + CompletionType.REFERENCE_NAME), + arguments( + "USE\n BRANCH\n \"main\"\n AT ", + true, + List.of(), + List.of(), + List.of(), + // Must not reset the completion type in the parser, so this value is expected here + CompletionType.REFERENCE_NAME), + arguments( + "CREATE BRANCH IF", true, List.of(), List.of(), List.of("CREATE BRANCH IF NOT"), null), + arguments( + "CREATE BRANCH IF ", true, List.of(), List.of(), List.of("CREATE BRANCH IF NOT"), null), + arguments( + "CREATE BRANCH IF NOT", + true, + List.of(), + List.of(), + List.of("CREATE BRANCH IF NOT EXISTS"), + null), + arguments( + "CREATE BRANCH IF NOT ", + true, + List.of(), + List.of(), + List.of("CREATE BRANCH IF NOT EXISTS"), + null), + arguments( + "CREATE BRANCH IF NOT EXISTS", + true, + List.of(), + List.of(), + List.of(), + // Must not reset the completion type in the parser, so this value is expected here + CompletionType.NON_EXISTING_REFERENCE_NAME)); + } + + @ParameterizedTest + @MethodSource + public void parse(String input, List expectedSpecs) { + soft.assertThatCode( + () -> { + NessieCliLexer lexer = new NessieCliLexer(input); + NessieCliParser parser = new NessieCliParser(lexer); + + parser.Script(); + Node node = parser.rootNode(); + Script script = (Script) node; + List commandSpecs = + script.getCommandSpecs().stream().map(TestCliCompleter::asImmutable).toList(); + soft.assertThat(commandSpecs).containsExactlyElementsOf(expectedSpecs); + }) + .doesNotThrowAnyException(); + + if (expectedSpecs.size() == 1) { + soft.assertThatCode( + () -> { + NessieCliLexer lexer = new NessieCliLexer(input); + NessieCliParser parser = new NessieCliParser(lexer); + + parser.SingleStatement(); + Node node = parser.rootNode(); + SingleStatement singleStatement = (SingleStatement) node; + CommandSpec commandSpec = singleStatement.getCommandSpec(); + commandSpec = asImmutable(commandSpec); + soft.assertThat(commandSpec).isEqualTo(expectedSpecs.get(0)); + }) + .doesNotThrowAnyException(); + } + } + + static Stream parse() { + return Stream.of( + arguments( + "CONNECT TO \"http://foo.bar:1234/api/v2\"", + List.of( + ImmutableConnectCommandSpec.builder().uri("http://foo.bar:1234/api/v2").build())), + arguments( + "CONNECT TO https://foo.bar/x/y/zapi/v2 USING a=b AND c=d", + List.of( + ImmutableConnectCommandSpec.builder() + .uri("https://foo.bar/x/y/zapi/v2") + .putParameter("a", "b") + .putParameter("c", "d") + .build())), + arguments("HELP", List.of(ImmutableHelpCommandSpec.builder().build())), + arguments( + "HELP; EXIT;", + List.of( + ImmutableHelpCommandSpec.builder().build(), + ImmutableExitCommandSpec.builder().build())), + // TODO move those to a separate test + // arguments("HELP create branch;", List.of(ImmutableHelpCommandSpec.builder().build())), + // arguments("HELP use;", List.of(ImmutableHelpCommandSpec.builder().build())), + // arguments("HELP create;", List.of(ImmutableHelpCommandSpec.builder().build())), + arguments("show log", List.of(ImmutableShowLogCommandSpec.of(null, null))), + arguments("show reference", List.of(ImmutableShowReferenceCommandSpec.of(null, null))), + arguments("show log bar;", List.of(ImmutableShowLogCommandSpec.of(null, "bar"))), + arguments( + "show reference bar;", List.of(ImmutableShowReferenceCommandSpec.of(null, "bar"))), + arguments( + "cReAtE bRaNcH \"foo\";", + List.of(ImmutableCreateReferenceCommandSpec.of(null, "foo", "bRaNcH", false))), + arguments( + "use bRaNcH \"foo\";", + List.of(ImmutableUseReferenceCommandSpec.of(null, "foo", "bRaNcH"))), + arguments( + "DROP TAG bar;", + List.of(ImmutableDropReferenceCommandSpec.of(null, "bar", "TAG", false))), + arguments( + "CREATE BRANCH if not exists foo; DROP TAG bar;", + List.of( + ImmutableCreateReferenceCommandSpec.of(null, "foo", "BRANCH", true), + ImmutableDropReferenceCommandSpec.of(null, "bar", "TAG", false))), + arguments( + "create branch foo; drop tag bar;", + List.of( + ImmutableCreateReferenceCommandSpec.of(null, "foo", "branch", false), + ImmutableDropReferenceCommandSpec.of(null, "bar", "tag", false)))); + } + + @SuppressWarnings("unchecked") + static T asImmutable(T parsedSpec) { + return (T) + switch (parsedSpec.commandType()) { + case HELP -> + ImmutableHelpCommandSpec.builder() + .from((HelpCommandSpec) parsedSpec) + .sourceNode(null) + .build(); + case EXIT -> ImmutableExitCommandSpec.builder().build(); + case CONNECT -> + ImmutableConnectCommandSpec.builder() + .from((ConnectCommandSpec) parsedSpec) + .sourceNode(null) + .build(); + case ASSIGN_REFERENCE -> + ImmutableAssignReferenceCommandSpec.builder() + .from((AssignReferenceCommandSpec) parsedSpec) + .sourceNode(null) + .build(); + case USE_REFERENCE -> + ImmutableUseReferenceCommandSpec.builder() + .from((UseReferenceCommandSpec) parsedSpec) + .sourceNode(null) + .build(); + case LIST_REFERENCES -> + ImmutableListReferencesCommandSpec.builder() + .from((ListReferencesCommandSpec) parsedSpec) + .sourceNode(null) + .build(); + case SHOW_REFERENCE -> + ImmutableShowReferenceCommandSpec.builder() + .from((ShowReferenceCommandSpec) parsedSpec) + .sourceNode(null) + .build(); + case SHOW_LOG -> + ImmutableShowLogCommandSpec.builder() + .from((ShowLogCommandSpec) parsedSpec) + .sourceNode(null) + .build(); + case DROP_REFERENCE -> + ImmutableDropReferenceCommandSpec.builder() + .from((DropReferenceCommandSpec) parsedSpec) + .sourceNode(null) + .build(); + case CREATE_REFERENCE -> + ImmutableCreateReferenceCommandSpec.builder() + .from((CreateReferenceCommandSpec) parsedSpec) + .sourceNode(null) + .build(); + default -> throw new IllegalArgumentException("Unknown type " + parsedSpec.commandType()); + }; + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 38db9de5297..d6adff2809d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,6 +49,7 @@ cassandra-driver-bom = { module = "com.datastax.oss:java-driver-bom", version = caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version = "3.1.8" } cel-bom = { module = "org.projectnessie.cel:cel-bom", version = "0.4.4" } checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.ref = "checkstyle" } +congocc = { module = "org.congocc:org.congocc.parser.generator", version = "2.0.0-RC7" } docker-java-api = { module = "com.github.docker-java:docker-java-api", version = "3.3.6" } errorprone-annotations = { module = "com.google.errorprone:error_prone_annotations", version.ref = "errorprone" } errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone" } @@ -83,11 +84,13 @@ jakarta-servlet-api = { module = "jakarta.servlet:jakarta.servlet-api", version jakarta-validation-api = { module = "jakarta.validation:jakarta.validation-api", version = "3.0.2" } jakarta-ws-rs-api = { module = "jakarta.ws.rs:jakarta.ws.rs-api", version = "3.1.0" } jandex = { module = "org.jboss:jandex", version.ref = "jandex" } +jansi = { module = "org.fusesource.jansi:jansi", version = "2.4.1" } javax-validation-api = { module = "javax.validation:validation-api", version = "2.0.1.Final"} javax-ws-rs = { module = "javax.ws.rs:javax.ws.rs-api", version = "2.1.1" } jaxb-impl = { module = "com.sun.xml.bind:jaxb-impl", version = "4.0.5" } jersey-bom = { module = "org.glassfish.jersey:jersey-bom", version = "3.1.6" } jetbrains-annotations = { module = "org.jetbrains:annotations", version = "24.1.0" } +jline = { module = "org.jline:jline", version = "3.25.1" } jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } jmh-generator-annprocess = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" } junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } diff --git a/gradle/projects.main.properties b/gradle/projects.main.properties index 3ee3acf8a64..c4600c4b11e 100644 --- a/gradle/projects.main.properties +++ b/gradle/projects.main.properties @@ -1,5 +1,7 @@ nessie-azurite-testcontainer=testing/azurite-container nessie-bom=bom +nessie-cli=cli/cli +nessie-cli-grammar=cli/grammar nessie-client=api/client nessie-client-testextension=api/client-testextension nessie-combined-cs=testing/combined-cs