From 4118dcc76ee27659b3821f17f87b685855ce4c21 Mon Sep 17 00:00:00 2001 From: Ioannis Canellos Date: Thu, 18 May 2023 15:51:42 +0300 Subject: [PATCH] fix: detection of project root in qcli --- .../io/quarkus/cli/plugin/CatalogService.java | 20 +- .../io/quarkus/cli/plugin/CatalogUtil.java | 44 +++++ .../cli/plugin/PluginCatalogService.java | 2 +- .../io/quarkus/cli/plugin/PluginManager.java | 26 ++- .../quarkus/cli/plugin/PluginMangerState.java | 4 +- .../cli/plugin/PluginCatalogServiceTest.java | 173 ++++++++++++++++++ .../io/quarkus/cli/plugin/PluginUtilTest.java | 0 7 files changed, 248 insertions(+), 21 deletions(-) create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/CatalogUtil.java create mode 100644 independent-projects/tools/devtools-common/src/test/java/io/quarkus/cli/plugin/PluginCatalogServiceTest.java delete mode 100644 independent-projects/tools/devtools-common/src/test/java/io/quarkus/cli/plugin/PluginUtilTest.java diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/CatalogService.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/CatalogService.java index 96939bc83dcf43..659c893bd65b53 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/CatalogService.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/CatalogService.java @@ -15,11 +15,12 @@ public class CatalogService> { - private static final Predicate EXISTS_AND_WRITABLE = p -> p != null && p.toFile().exists() && p.toFile().canRead() - && p.toFile().canWrite(); - + protected static final Path USER_HOME = Paths.get(System.getProperty("user.home")); protected static final Predicate GIT_ROOT = p -> p != null && p.resolve(".git").toFile().exists(); + protected static final Predicate EXISTS_AND_WRITABLE = p -> p != null && p.toFile().exists() && p.toFile().canRead() + && p.toFile().canWrite(); + protected final ObjectMapper objectMapper = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .enable(SerializationFeature.INDENT_OUTPUT) @@ -60,16 +61,7 @@ public Optional readProjectCatalog(Optional dir) { * @return the catalog path wrapped as {@link Optional} or empty if the catalog does not exist. */ public Optional findProjectCatalogPath(Path dir) { - Optional catalogPath = Optional.of(dir).map(relativePath).filter(EXISTS_AND_WRITABLE); - if (catalogPath.isPresent()) { - return catalogPath; - } - if (projectRoot.test(dir)) { - return Optional.of(dir).map(relativePath); - } - return Optional.ofNullable(dir).map(Path::getParent) - .filter(EXISTS_AND_WRITABLE) - .flatMap(this::findProjectCatalogPath); + return CatalogUtil.findProjectRoot(dir).map(relativePath); } public Optional findProjectCatalogPath(Optional dir) { @@ -132,7 +124,7 @@ public void writeCatalog(T catalog) { * @return the catalog path wrapped as {@link Optional} or empty if the catalog does not exist. */ public Path getUserCatalogPath(Optional userDir) { - return relativePath.apply(userDir.orElse(Paths.get(System.getProperty("user.home")))); + return relativePath.apply(userDir.orElse(USER_HOME)); } /** diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/CatalogUtil.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/CatalogUtil.java new file mode 100644 index 00000000000000..7ca4f0ff84429e --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/CatalogUtil.java @@ -0,0 +1,44 @@ +package io.quarkus.cli.plugin; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.function.Predicate; + +public class CatalogUtil { + protected static final Path USER_HOME = Paths.get(System.getProperty("user.home")); + + protected static final Predicate EXISTS_AND_WRITABLE = p -> p != null && p.toFile().exists() && p.toFile().canRead() + && p.toFile().canWrite(); + protected static final Predicate IS_USER_HOME = p -> USER_HOME.equals(p); + protected static final Predicate IS_ELIGIBLE_PROJECT_ROOT = EXISTS_AND_WRITABLE.and(Predicate.not(IS_USER_HOME)); + protected static final Predicate GIT_ROOT = p -> p != null && p.resolve(".git").toFile().exists(); + protected static final Predicate HAS_POM_XML = p -> p != null && p.resolve("pom.xml").toFile().exists(); + protected static final Predicate HAS_BUILD_GRADLE = p -> p != null && p.resolve("build.gradle").toFile().exists(); + + /** + * Get the project root of the specified path. + * The method will traverse from the specified path up to upmost directory that the user can write and + * is under version control. + * + * @param dir the specified path + * @return the project path wrapped as {@link Optional} or empty if the catalog does not exist. + */ + public static Optional findProjectRoot(Path dir) { + Optional lastKnownProjectDirectory = Optional.empty(); + for (Path current = dir; IS_ELIGIBLE_PROJECT_ROOT.test(current); current = current.getParent()) { + if (GIT_ROOT.test(current)) { + return Optional.of(current); + } + + if (HAS_POM_XML.test(current)) { + lastKnownProjectDirectory = Optional.of(current); + } + + if (HAS_BUILD_GRADLE.test(current)) { + lastKnownProjectDirectory = Optional.of(current); + } + } + return lastKnownProjectDirectory; + } +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginCatalogService.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginCatalogService.java index cae25b1c60a185..7f775088d21504 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginCatalogService.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginCatalogService.java @@ -10,7 +10,7 @@ public class PluginCatalogService extends CatalogService { - private static final Function RELATIVE_CATALOG_JSON = p -> p.resolve(".quarkus").resolve("cli") + static final Function RELATIVE_CATALOG_JSON = p -> p.resolve(".quarkus").resolve("cli") .resolve("plugins").resolve("quarkus-cli-catalog.json"); public PluginCatalogService() { diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginManager.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginManager.java index 3b48f8383bc64f..dc1f0633cf0877 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginManager.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginManager.java @@ -1,9 +1,11 @@ package io.quarkus.cli.plugin; +import java.io.File; import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -27,19 +29,19 @@ public synchronized static PluginManager get() { } public synchronized static PluginManager create(PluginManagerSettings settings, MessageWriter output, - Optional userHome, Optional projectRoot, Supplier quarkusProject) { + Optional userHome, Optional currentDir, Supplier quarkusProject) { if (INSTANCE == null) { - INSTANCE = new PluginManager(settings, output, userHome, projectRoot, quarkusProject); + INSTANCE = new PluginManager(settings, output, userHome, currentDir, quarkusProject); } return INSTANCE; } PluginManager(PluginManagerSettings settings, MessageWriter output, Optional userHome, - Optional projectRoot, Supplier quarkusProject) { + Optional currentDir, Supplier quarkusProject) { this.settings = settings; this.output = output; this.util = PluginManagerUtil.getUtil(settings); - this.state = new PluginMangerState(settings, output, userHome, projectRoot, quarkusProject); + this.state = new PluginMangerState(settings, output, userHome, currentDir, quarkusProject); } /** @@ -303,6 +305,22 @@ public boolean syncIfNeeded() { //syncing may require user interaction, so just return false return false; } + + // Check if there project catalog file is missing + boolean createdMissingProjectCatalog = state.getPluginCatalogService().findProjectCatalogPath(state.getProjectRoot()) + .map(Path::toFile) + .filter(Predicate.not(File::exists)) + .map(File::toPath) + .map(p -> { + output.info("Project plugin catalog has not been initialized. Initializing!"); + state.getPluginCatalogService().writeCatalog(new PluginCatalog().withCatalogLocation(p)); + return true; + }).orElse(false); + + if (createdMissingProjectCatalog) { + return sync(); + } + PluginCatalog catalog = state.getCombinedCatalog(); if (PluginUtil.shouldSync(state.getProjectRoot(), catalog)) { output.info("Plugin catalog last updated on: " + catalog.getLastUpdate() + ". Syncing!"); diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginMangerState.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginMangerState.java index 4979d89d66664e..4f838713b76e26 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginMangerState.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginMangerState.java @@ -19,7 +19,7 @@ class PluginMangerState { - PluginMangerState(PluginManagerSettings settings, MessageWriter output, Optional userHome, Optional projectRoot, + PluginMangerState(PluginManagerSettings settings, MessageWriter output, Optional userHome, Optional currentDir, Supplier quarkusProject) { this.settings = settings; this.output = output; @@ -27,11 +27,11 @@ class PluginMangerState { this.quarkusProject = quarkusProject; //Inferred - this.projectRoot = projectRoot.filter(p -> !p.equals(userHome.orElse(null))); this.jbangCatalogService = new JBangCatalogService(settings.isInteractiveMode(), output, settings.getPluginPrefix(), settings.getFallbackJBangCatalog(), settings.getRemoteJBangCatalogs()); this.pluginCatalogService = new PluginCatalogService(settings.getToRelativePath()); + this.projectRoot = currentDir.flatMap(CatalogUtil::findProjectRoot); this.util = PluginManagerUtil.getUtil(settings); } diff --git a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/cli/plugin/PluginCatalogServiceTest.java b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/cli/plugin/PluginCatalogServiceTest.java new file mode 100644 index 00000000000000..4ee9b8f38162e9 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/cli/plugin/PluginCatalogServiceTest.java @@ -0,0 +1,173 @@ +package io.quarkus.cli.plugin; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +public class PluginCatalogServiceTest { + + PluginCatalogService service = new PluginCatalogService(); + + Path rootDir; + + @BeforeEach + public void setUp() throws Exception { + rootDir = Files.createTempDirectory("quarkus-cli-test-project-root"); + } + + @AfterEach + public void cleanUp() throws Exception { + makeWritable(rootDir); + } + + @Test + public void shouldFindGitRootCatalogPath() throws Exception { + Path expectedCatalogPath = PluginCatalogService.RELATIVE_CATALOG_JSON.apply(rootDir); + Path moduleA = rootDir.resolve("module-a"); + Path moduleAA = moduleA.resolve("module-aa"); + Path dotGit = rootDir.resolve(".git"); + dotGit.toFile().mkdir(); + moduleAA.toFile().mkdirs(); + + Optional result = service.findProjectCatalogPath(rootDir); + assertEquals(expectedCatalogPath, result.get()); + + result = service.findProjectCatalogPath(moduleA); + assertEquals(expectedCatalogPath, result.get()); + } + + @Test + public void shouldFindDotQuakursRootCatalogPath() throws Exception { + Path moduleB = rootDir.resolve("module-b"); + Path moduleBA = moduleB.resolve("module-ba"); + + Path dotGit = rootDir.resolve(".git"); + dotGit.toFile().mkdir(); + + Path dotQuarkus = PluginCatalogService.RELATIVE_CATALOG_JSON.apply(moduleB); + dotQuarkus.getParent().toFile().mkdirs(); + Files.write(dotQuarkus, new byte[0]); + + moduleBA.toFile().mkdirs(); + + Path expectedCatalogPath = PluginCatalogService.RELATIVE_CATALOG_JSON.apply(rootDir); + + Optional result = service.findProjectCatalogPath(moduleB); + assertEquals(expectedCatalogPath, result.get()); + + result = service.findProjectCatalogPath(moduleBA); + assertEquals(expectedCatalogPath, result.get()); + } + + @Test + @DisabledOnOs(OS.WINDOWS) //Test changes File permissions + public void shouldFindLastReadableCatalogPath() throws Exception { + + Path moduleC = rootDir.resolve("module-c"); + Path moduleCA = moduleC.resolve("module-ca"); + + Path dotGit = rootDir.resolve(".git"); + dotGit.toFile().mkdir(); + + moduleCA.toFile().mkdirs(); + // Parent not readable + try { + if (moduleC.toFile().setWritable(false)) { + Optional result = service.findProjectCatalogPath(moduleCA); + assertTrue(result.isEmpty()); + } + } finally { + moduleC.toFile().setWritable(true); + } + } + + @Test + @DisabledOnOs(OS.WINDOWS) //Test changes File permissions + public void shouldFindLastMavenRootCatalogPath() throws Exception { + + Path moduleM = rootDir.resolve("module-m"); + Path moduleMA = moduleM.resolve("module-ma"); + Path moduleMAA = moduleMA.resolve("module-maa"); + + Path dotGit = rootDir.resolve(".git"); + dotGit.toFile().mkdir(); + + moduleMAA.toFile().mkdirs(); + + Path pomMA = moduleMA.resolve("pom.xml"); + Files.write(pomMA, new byte[0]); + + Path pomMAA = moduleMAA.resolve("pom.xml"); + Files.write(pomMAA, new byte[0]); + + Path expectedCatalogPath = PluginCatalogService.RELATIVE_CATALOG_JSON.apply(moduleMA); + + // Parent not readable + try { + if (rootDir.toFile().setWritable(false)) { + Optional result = service.findProjectCatalogPath(moduleMA); + assertEquals(expectedCatalogPath, result.get()); + } + } finally { + moduleM.toFile().setWritable(true); + } + Optional result = service.findProjectCatalogPath(moduleMAA); + assertEquals(expectedCatalogPath, result.get()); + } + + @Test + @DisabledOnOs(OS.WINDOWS) //Test changes File permissions + public void shouldFindLastGradleRootCatalogPath() throws Exception { + + Path moduleG = rootDir.resolve("module-g"); + Path moduleGA = moduleG.resolve("module-ga"); + Path moduleGAA = moduleGA.resolve("module-gaa"); + + Path dotGit = rootDir.resolve(".git"); + dotGit.toFile().mkdir(); + + moduleGAA.toFile().mkdirs(); + + Path pomGA = moduleGA.resolve("build.gradle"); + Files.write(pomGA, new byte[0]); + + Path pomGAA = moduleGAA.resolve("build.gradle"); + Files.write(pomGAA, new byte[0]); + + Path expectedCatalogPath = PluginCatalogService.RELATIVE_CATALOG_JSON.apply(moduleGA); + + // Parent not readable + try { + if (rootDir.toFile().setWritable(false)) { + Optional result = service.findProjectCatalogPath(moduleGA); + assertEquals(expectedCatalogPath, result.get()); + } + } finally { + moduleG.toFile().setWritable(true); + } + Optional result = service.findProjectCatalogPath(moduleGAA); + assertEquals(expectedCatalogPath, result.get()); + } + + private static void makeWritable(Path path) throws IOException { + try (DirectoryStream stream = Files.newDirectoryStream(path)) { + for (Path sub : stream) { + if (Files.isDirectory(sub)) { + makeWritable(sub); + } + } + path.toFile().setWritable(true); + } + } +} diff --git a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/cli/plugin/PluginUtilTest.java b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/cli/plugin/PluginUtilTest.java deleted file mode 100644 index e69de29bb2d1d6..00000000000000