Skip to content

Commit 1c8d564

Browse files
authored
Merge pull request #33469 from iocanel/cli-projet-root-detection-and-syncing
Fix detection of project root in Quarkus CLI
2 parents 489372f + 9d578b2 commit 1c8d564

File tree

6 files changed

+234
-19
lines changed

6 files changed

+234
-19
lines changed

independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/CatalogService.java

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,14 @@
1515

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

18-
private static final Predicate<Path> EXISTS_AND_WRITABLE = p -> p != null && p.toFile().exists() && p.toFile().canRead()
18+
protected static final Path USER_HOME = Paths.get(System.getProperty("user.home"));
19+
20+
protected static final Predicate<Path> EXISTS_AND_WRITABLE = p -> p != null && p.toFile().exists() && p.toFile().canRead()
1921
&& p.toFile().canWrite();
22+
protected static final Predicate<Path> IS_USER_HOME = p -> USER_HOME.equals(p);
23+
protected static final Predicate<Path> IS_ELIGIBLE_PROJECT_ROOT = EXISTS_AND_WRITABLE.and(Predicate.not(IS_USER_HOME));
24+
protected static final Predicate<Path> HAS_POM_XML = p -> p != null && p.resolve("pom.xml").toFile().exists();
25+
protected static final Predicate<Path> HAS_BUILD_GRADLE = p -> p != null && p.resolve("build.gradle").toFile().exists();
2026

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

@@ -60,16 +66,7 @@ public Optional<T> readProjectCatalog(Optional<Path> dir) {
6066
* @return the catalog path wrapped as {@link Optional} or empty if the catalog does not exist.
6167
*/
6268
public Optional<Path> findProjectCatalogPath(Path dir) {
63-
Optional<Path> catalogPath = Optional.of(dir).map(relativePath).filter(EXISTS_AND_WRITABLE);
64-
if (catalogPath.isPresent()) {
65-
return catalogPath;
66-
}
67-
if (projectRoot.test(dir)) {
68-
return Optional.of(dir).map(relativePath);
69-
}
70-
return Optional.ofNullable(dir).map(Path::getParent)
71-
.filter(EXISTS_AND_WRITABLE)
72-
.flatMap(this::findProjectCatalogPath);
69+
return findProjectRoot(dir).map(relativePath);
7370
}
7471

7572
public Optional<Path> findProjectCatalogPath(Optional<Path> dir) {
@@ -132,7 +129,7 @@ public void writeCatalog(T catalog) {
132129
* @return the catalog path wrapped as {@link Optional} or empty if the catalog does not exist.
133130
*/
134131
public Path getUserCatalogPath(Optional<Path> userDir) {
135-
return relativePath.apply(userDir.orElse(Paths.get(System.getProperty("user.home"))));
132+
return relativePath.apply(userDir.orElse(USER_HOME));
136133
}
137134

138135
/**
@@ -177,4 +174,31 @@ public Optional<Path> getCatalogPath(Optional<Path> projectDir, Optional<Path> u
177174
return getRelativeCatalogPath(projectDir).filter(EXISTS_AND_WRITABLE)
178175
.or(() -> Optional.of(getUserCatalogPath(userDir)));
179176
}
177+
178+
/**
179+
* Get the project root of the specified path.
180+
* The method will traverse from the specified path up to upmost directory that the user can write and
181+
* is under version control.
182+
*
183+
* @param dir the specified path
184+
* @return the project path wrapped as {@link Optional} or empty if the catalog does not exist.
185+
*/
186+
public static Optional<Path> findProjectRoot(Path dir) {
187+
Optional<Path> lastKnownProjectDirectory = Optional.empty();
188+
for (Path current = dir; IS_ELIGIBLE_PROJECT_ROOT.test(current); current = current.getParent()) {
189+
if (GIT_ROOT.test(current)) {
190+
return Optional.of(current);
191+
}
192+
193+
if (HAS_POM_XML.test(current)) {
194+
lastKnownProjectDirectory = Optional.of(current);
195+
}
196+
197+
if (HAS_BUILD_GRADLE.test(current)) {
198+
lastKnownProjectDirectory = Optional.of(current);
199+
}
200+
}
201+
return lastKnownProjectDirectory;
202+
}
203+
180204
}

independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginCatalogService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
public class PluginCatalogService extends CatalogService<PluginCatalog> {
1212

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

1616
public PluginCatalogService() {

independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginManager.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package io.quarkus.cli.plugin;
22

3+
import java.io.File;
34
import java.nio.file.Path;
45
import java.util.List;
56
import java.util.Map;
67
import java.util.Optional;
8+
import java.util.function.Predicate;
79
import java.util.function.Supplier;
810
import java.util.stream.Collectors;
911

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

2931
public synchronized static PluginManager create(PluginManagerSettings settings, MessageWriter output,
30-
Optional<Path> userHome, Optional<Path> projectRoot, Supplier<QuarkusProject> quarkusProject) {
32+
Optional<Path> userHome, Optional<Path> currentDir, Supplier<QuarkusProject> quarkusProject) {
3133
if (INSTANCE == null) {
32-
INSTANCE = new PluginManager(settings, output, userHome, projectRoot, quarkusProject);
34+
INSTANCE = new PluginManager(settings, output, userHome, currentDir, quarkusProject);
3335
}
3436
return INSTANCE;
3537
}
3638

3739
PluginManager(PluginManagerSettings settings, MessageWriter output, Optional<Path> userHome,
38-
Optional<Path> projectRoot, Supplier<QuarkusProject> quarkusProject) {
40+
Optional<Path> currentDir, Supplier<QuarkusProject> quarkusProject) {
3941
this.settings = settings;
4042
this.output = output;
4143
this.util = PluginManagerUtil.getUtil(settings);
42-
this.state = new PluginMangerState(settings, output, userHome, projectRoot, quarkusProject);
44+
this.state = new PluginMangerState(settings, output, userHome, currentDir, quarkusProject);
4345
}
4446

4547
/**
@@ -303,6 +305,22 @@ public boolean syncIfNeeded() {
303305
//syncing may require user interaction, so just return false
304306
return false;
305307
}
308+
309+
// Check if there project catalog file is missing
310+
boolean createdMissingProjectCatalog = state.getPluginCatalogService().findProjectCatalogPath(state.getProjectRoot())
311+
.map(Path::toFile)
312+
.filter(Predicate.not(File::exists))
313+
.map(File::toPath)
314+
.map(p -> {
315+
output.info("Project plugin catalog has not been initialized. Initializing!");
316+
state.getPluginCatalogService().writeCatalog(new PluginCatalog().withCatalogLocation(p));
317+
return true;
318+
}).orElse(false);
319+
320+
if (createdMissingProjectCatalog) {
321+
return sync();
322+
}
323+
306324
PluginCatalog catalog = state.getCombinedCatalog();
307325
if (PluginUtil.shouldSync(state.getProjectRoot(), catalog)) {
308326
output.info("Plugin catalog last updated on: " + catalog.getLastUpdate() + ". Syncing!");

independent-projects/tools/devtools-common/src/main/java/io/quarkus/cli/plugin/PluginMangerState.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,19 @@
1919

2020
class PluginMangerState {
2121

22-
PluginMangerState(PluginManagerSettings settings, MessageWriter output, Optional<Path> userHome, Optional<Path> projectRoot,
22+
PluginMangerState(PluginManagerSettings settings, MessageWriter output, Optional<Path> userHome, Optional<Path> currentDir,
2323
Supplier<QuarkusProject> quarkusProject) {
2424
this.settings = settings;
2525
this.output = output;
2626
this.userHome = userHome;
2727
this.quarkusProject = quarkusProject;
2828

2929
//Inferred
30-
this.projectRoot = projectRoot.filter(p -> !p.equals(userHome.orElse(null)));
3130
this.jbangCatalogService = new JBangCatalogService(settings.isInteractiveMode(), output, settings.getPluginPrefix(),
3231
settings.getFallbackJBangCatalog(),
3332
settings.getRemoteJBangCatalogs());
3433
this.pluginCatalogService = new PluginCatalogService(settings.getToRelativePath());
34+
this.projectRoot = currentDir.flatMap(CatalogService::findProjectRoot);
3535
this.util = PluginManagerUtil.getUtil(settings);
3636
}
3737

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package io.quarkus.cli.plugin;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import java.io.IOException;
7+
import java.nio.file.DirectoryStream;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
import java.util.Optional;
11+
12+
import org.junit.jupiter.api.AfterEach;
13+
import org.junit.jupiter.api.BeforeEach;
14+
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.api.condition.DisabledOnOs;
16+
import org.junit.jupiter.api.condition.OS;
17+
18+
public class PluginCatalogServiceTest {
19+
20+
PluginCatalogService service = new PluginCatalogService();
21+
22+
Path rootDir;
23+
24+
@BeforeEach
25+
public void setUp() throws Exception {
26+
rootDir = Files.createTempDirectory("quarkus-cli-test-project-root");
27+
}
28+
29+
@AfterEach
30+
public void cleanUp() throws Exception {
31+
makeWritable(rootDir);
32+
}
33+
34+
@Test
35+
public void shouldFindGitRootCatalogPath() throws Exception {
36+
Path expectedCatalogPath = PluginCatalogService.RELATIVE_CATALOG_JSON.apply(rootDir);
37+
Path moduleA = rootDir.resolve("module-a");
38+
Path moduleAA = moduleA.resolve("module-aa");
39+
Path dotGit = rootDir.resolve(".git");
40+
dotGit.toFile().mkdir();
41+
moduleAA.toFile().mkdirs();
42+
43+
Optional<Path> result = service.findProjectCatalogPath(rootDir);
44+
assertEquals(expectedCatalogPath, result.get());
45+
46+
result = service.findProjectCatalogPath(moduleA);
47+
assertEquals(expectedCatalogPath, result.get());
48+
}
49+
50+
@Test
51+
public void shouldFindDotQuakursRootCatalogPath() throws Exception {
52+
Path moduleB = rootDir.resolve("module-b");
53+
Path moduleBA = moduleB.resolve("module-ba");
54+
55+
Path dotGit = rootDir.resolve(".git");
56+
dotGit.toFile().mkdir();
57+
58+
Path dotQuarkus = PluginCatalogService.RELATIVE_CATALOG_JSON.apply(moduleB);
59+
dotQuarkus.getParent().toFile().mkdirs();
60+
Files.write(dotQuarkus, new byte[0]);
61+
62+
moduleBA.toFile().mkdirs();
63+
64+
Path expectedCatalogPath = PluginCatalogService.RELATIVE_CATALOG_JSON.apply(rootDir);
65+
66+
Optional<Path> result = service.findProjectCatalogPath(moduleB);
67+
assertEquals(expectedCatalogPath, result.get());
68+
69+
result = service.findProjectCatalogPath(moduleBA);
70+
assertEquals(expectedCatalogPath, result.get());
71+
}
72+
73+
@Test
74+
@DisabledOnOs(OS.WINDOWS) //Test changes File permissions
75+
public void shouldFindLastReadableCatalogPath() throws Exception {
76+
77+
Path moduleC = rootDir.resolve("module-c");
78+
Path moduleCA = moduleC.resolve("module-ca");
79+
80+
Path dotGit = rootDir.resolve(".git");
81+
dotGit.toFile().mkdir();
82+
83+
moduleCA.toFile().mkdirs();
84+
// Parent not readable
85+
try {
86+
if (moduleC.toFile().setWritable(false)) {
87+
Optional<Path> result = service.findProjectCatalogPath(moduleCA);
88+
assertTrue(result.isEmpty());
89+
}
90+
} finally {
91+
moduleC.toFile().setWritable(true);
92+
}
93+
}
94+
95+
@Test
96+
@DisabledOnOs(OS.WINDOWS) //Test changes File permissions
97+
public void shouldFindLastMavenRootCatalogPath() throws Exception {
98+
99+
Path moduleM = rootDir.resolve("module-m");
100+
Path moduleMA = moduleM.resolve("module-ma");
101+
Path moduleMAA = moduleMA.resolve("module-maa");
102+
103+
Path dotGit = rootDir.resolve(".git");
104+
dotGit.toFile().mkdir();
105+
106+
moduleMAA.toFile().mkdirs();
107+
108+
Path pomMA = moduleMA.resolve("pom.xml");
109+
Files.write(pomMA, new byte[0]);
110+
111+
Path pomMAA = moduleMAA.resolve("pom.xml");
112+
Files.write(pomMAA, new byte[0]);
113+
114+
Path expectedCatalogPath = PluginCatalogService.RELATIVE_CATALOG_JSON.apply(moduleMA);
115+
116+
// Parent not readable
117+
try {
118+
if (rootDir.toFile().setWritable(false)) {
119+
Optional<Path> result = service.findProjectCatalogPath(moduleMA);
120+
assertEquals(expectedCatalogPath, result.get());
121+
}
122+
} finally {
123+
moduleM.toFile().setWritable(true);
124+
}
125+
Optional<Path> result = service.findProjectCatalogPath(moduleMAA);
126+
assertEquals(expectedCatalogPath, result.get());
127+
}
128+
129+
@Test
130+
@DisabledOnOs(OS.WINDOWS) //Test changes File permissions
131+
public void shouldFindLastGradleRootCatalogPath() throws Exception {
132+
133+
Path moduleG = rootDir.resolve("module-g");
134+
Path moduleGA = moduleG.resolve("module-ga");
135+
Path moduleGAA = moduleGA.resolve("module-gaa");
136+
137+
Path dotGit = rootDir.resolve(".git");
138+
dotGit.toFile().mkdir();
139+
140+
moduleGAA.toFile().mkdirs();
141+
142+
Path pomGA = moduleGA.resolve("build.gradle");
143+
Files.write(pomGA, new byte[0]);
144+
145+
Path pomGAA = moduleGAA.resolve("build.gradle");
146+
Files.write(pomGAA, new byte[0]);
147+
148+
Path expectedCatalogPath = PluginCatalogService.RELATIVE_CATALOG_JSON.apply(moduleGA);
149+
150+
// Parent not readable
151+
try {
152+
if (rootDir.toFile().setWritable(false)) {
153+
Optional<Path> result = service.findProjectCatalogPath(moduleGA);
154+
assertEquals(expectedCatalogPath, result.get());
155+
}
156+
} finally {
157+
moduleG.toFile().setWritable(true);
158+
}
159+
Optional<Path> result = service.findProjectCatalogPath(moduleGAA);
160+
assertEquals(expectedCatalogPath, result.get());
161+
}
162+
163+
private static void makeWritable(Path path) throws IOException {
164+
try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
165+
for (Path sub : stream) {
166+
if (Files.isDirectory(sub)) {
167+
makeWritable(sub);
168+
}
169+
}
170+
path.toFile().setWritable(true);
171+
}
172+
}
173+
}

independent-projects/tools/devtools-common/src/test/java/io/quarkus/cli/plugin/PluginUtilTest.java

Whitespace-only changes.

0 commit comments

Comments
 (0)