Skip to content

Commit

Permalink
Merge pull request #33469 from iocanel/cli-projet-root-detection-and-…
Browse files Browse the repository at this point in the history
…syncing

Fix detection of project root in Quarkus CLI
  • Loading branch information
iocanel authored Jun 14, 2023
2 parents 489372f + 9d578b2 commit 1c8d564
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,14 @@

public class CatalogService<T extends Catalog<T>> {

private static final Predicate<Path> EXISTS_AND_WRITABLE = p -> p != null && p.toFile().exists() && p.toFile().canRead()
protected static final Path USER_HOME = Paths.get(System.getProperty("user.home"));

protected static final Predicate<Path> EXISTS_AND_WRITABLE = p -> p != null && p.toFile().exists() && p.toFile().canRead()
&& p.toFile().canWrite();
protected static final Predicate<Path> IS_USER_HOME = p -> USER_HOME.equals(p);
protected static final Predicate<Path> IS_ELIGIBLE_PROJECT_ROOT = EXISTS_AND_WRITABLE.and(Predicate.not(IS_USER_HOME));
protected static final Predicate<Path> HAS_POM_XML = p -> p != null && p.resolve("pom.xml").toFile().exists();
protected static final Predicate<Path> HAS_BUILD_GRADLE = p -> p != null && p.resolve("build.gradle").toFile().exists();

protected static final Predicate<Path> GIT_ROOT = p -> p != null && p.resolve(".git").toFile().exists();

Expand Down Expand Up @@ -60,16 +66,7 @@ public Optional<T> readProjectCatalog(Optional<Path> dir) {
* @return the catalog path wrapped as {@link Optional} or empty if the catalog does not exist.
*/
public Optional<Path> findProjectCatalogPath(Path dir) {
Optional<Path> 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 findProjectRoot(dir).map(relativePath);
}

public Optional<Path> findProjectCatalogPath(Optional<Path> dir) {
Expand Down Expand Up @@ -132,7 +129,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<Path> userDir) {
return relativePath.apply(userDir.orElse(Paths.get(System.getProperty("user.home"))));
return relativePath.apply(userDir.orElse(USER_HOME));
}

/**
Expand Down Expand Up @@ -177,4 +174,31 @@ public Optional<Path> getCatalogPath(Optional<Path> projectDir, Optional<Path> u
return getRelativeCatalogPath(projectDir).filter(EXISTS_AND_WRITABLE)
.or(() -> Optional.of(getUserCatalogPath(userDir)));
}

/**
* 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<Path> findProjectRoot(Path dir) {
Optional<Path> 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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

public class PluginCatalogService extends CatalogService<PluginCatalog> {

private static final Function<Path, Path> RELATIVE_CATALOG_JSON = p -> p.resolve(".quarkus").resolve("cli")
static final Function<Path, Path> RELATIVE_CATALOG_JSON = p -> p.resolve(".quarkus").resolve("cli")
.resolve("plugins").resolve("quarkus-cli-catalog.json");

public PluginCatalogService() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -27,19 +29,19 @@ public synchronized static PluginManager get() {
}

public synchronized static PluginManager create(PluginManagerSettings settings, MessageWriter output,
Optional<Path> userHome, Optional<Path> projectRoot, Supplier<QuarkusProject> quarkusProject) {
Optional<Path> userHome, Optional<Path> currentDir, Supplier<QuarkusProject> 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<Path> userHome,
Optional<Path> projectRoot, Supplier<QuarkusProject> quarkusProject) {
Optional<Path> currentDir, Supplier<QuarkusProject> 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);
}

/**
Expand Down Expand Up @@ -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!");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,19 @@

class PluginMangerState {

PluginMangerState(PluginManagerSettings settings, MessageWriter output, Optional<Path> userHome, Optional<Path> projectRoot,
PluginMangerState(PluginManagerSettings settings, MessageWriter output, Optional<Path> userHome, Optional<Path> currentDir,
Supplier<QuarkusProject> quarkusProject) {
this.settings = settings;
this.output = output;
this.userHome = userHome;
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(CatalogService::findProjectRoot);
this.util = PluginManagerUtil.getUtil(settings);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Path> 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<Path> 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<Path> 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<Path> result = service.findProjectCatalogPath(moduleMA);
assertEquals(expectedCatalogPath, result.get());
}
} finally {
moduleM.toFile().setWritable(true);
}
Optional<Path> 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<Path> result = service.findProjectCatalogPath(moduleGA);
assertEquals(expectedCatalogPath, result.get());
}
} finally {
moduleG.toFile().setWritable(true);
}
Optional<Path> result = service.findProjectCatalogPath(moduleGAA);
assertEquals(expectedCatalogPath, result.get());
}

private static void makeWritable(Path path) throws IOException {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
for (Path sub : stream) {
if (Files.isDirectory(sub)) {
makeWritable(sub);
}
}
path.toFile().setWritable(true);
}
}
}
Empty file.

0 comments on commit 1c8d564

Please sign in to comment.