Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
347a20d
#1166: import maven repos in intellij by editing misc.xml
cthies-capgemini Aug 27, 2025
8ad3f85
Merge branch 'devonfw:main' into feature/1166-automatic-project-impor…
cthies-capgemini Sep 3, 2025
12f8664
#1166: import gradle repos in intellij by editing gradle.xml
cthies-capgemini Sep 5, 2025
bb6c324
Merge remote-tracking branch 'origin/feature/1166-automatic-project-i…
cthies-capgemini Sep 5, 2025
7d8681f
#1166: fix variable names
cthies-capgemini Sep 8, 2025
8656056
#1166: add support for gradle projects in kotlin
cthies-capgemini Sep 8, 2025
3ff481e
#1166: use environment variables for repository path
cthies-capgemini Sep 10, 2025
9ee0393
Merge branch 'main' into feature/1166-automatic-project-import-for-in…
cthies-capgemini Sep 10, 2025
89e0877
#1166: remove debug prints
cthies-capgemini Sep 10, 2025
a64e626
#1166: use intellij project path
cthies-capgemini Sep 10, 2025
e942333
Merge remote-tracking branch 'origin/feature/1166-automatic-project-i…
cthies-capgemini Sep 10, 2025
075a6c7
Merge branch 'main' into feature/1166-automatic-project-import-for-in…
cthies-capgemini Sep 22, 2025
2178218
#1166: replace specific intellij env variables with general extensibl…
cthies-capgemini Sep 22, 2025
4d93673
Merge remote-tracking branch 'origin/feature/1166-automatic-project-i…
cthies-capgemini Sep 22, 2025
ea2748c
#1166: fix grammar in comments
cthies-capgemini Sep 23, 2025
3fa4098
#1166: inherit from EnvironmentVariablesResolved instead of AbstractE…
cthies-capgemini Sep 23, 2025
e9955e6
#1166: remove redundant methods and add javadoc
cthies-capgemini Sep 24, 2025
e7d1ee5
Merge branch 'devonfw:main' into feature/1166-automatic-project-impor…
cthies-capgemini Sep 24, 2025
91e6e65
Merge branch 'main' into feature/1166-automatic-project-import-for-in…
cthies-capgemini Sep 29, 2025
d4a3564
Update cli/src/main/java/com/devonfw/tools/ide/environment/Extensible…
cthies-capgemini Sep 30, 2025
72d71da
#1166: prevent null values for env variables
cthies-capgemini Sep 30, 2025
038beb1
#1166: clean up how file paths are handled
cthies-capgemini Sep 30, 2025
d58a52f
#1166: prevent error from missing template file
cthies-capgemini Sep 30, 2025
f0febf8
#1166: use parent of environment variables
cthies-capgemini Sep 30, 2025
870907b
#1166: shorten file creation
cthies-capgemini Sep 30, 2025
2e949b3
#1166: improve path handling
cthies-capgemini Sep 30, 2025
970a67e
#1166: fix thinking error
cthies-capgemini Sep 30, 2025
f980d33
Merge branch 'main' into feature/1166-automatic-project-import-for-in…
jan-vcapgemini Oct 13, 2025
d90894d
Merge branch 'main' into feature/1166-automatic-project-import-for-in…
hohwille Oct 14, 2025
cd87834
Merge branch 'main' into feature/1166-automatic-project-import-for-in…
hohwille Oct 17, 2025
564234c
Merge branch 'main' into feature/1166-automatic-project-import-for-in…
jan-vcapgemini Oct 28, 2025
fa2a9b6
Merge branch 'main' into feature/1166-automatic-project-import-for-in…
jan-vcapgemini Oct 28, 2025
7a77a64
Merge branch 'main' into feature/1166-automatic-project-import-for-in…
jan-vcapgemini Nov 17, 2025
7bb8b66
#1166: Implemented requested changes
jan-vcapgemini Nov 17, 2025
1574e0b
#1166: Implemented requested changes
jan-vcapgemini Nov 17, 2025
c25c9b7
Merge branch 'main' into feature/1166-automatic-project-import-for-in…
jan-vcapgemini Nov 25, 2025
c885ada
#1166: Implemented requested changes
jan-vcapgemini Nov 25, 2025
081c8ec
Merge branch 'main' into feature/1166-automatic-project-import-for-in…
jan-vcapgemini Nov 25, 2025
a728cf8
#1166: Fixed XmlMerger test
jan-vcapgemini Nov 25, 2025
992b2b0
#1166: Add changelog entry
jan-vcapgemini Nov 25, 2025
27b9af9
Merge branch 'main' into feature/1166-automatic-project-import-for-in…
hohwille Nov 30, 2025
17b7d05
Merge branch 'main' into feature/1166-automatic-project-import-for-in…
hohwille Dec 1, 2025
8f5bcaf
#1166: moved to release 2025.12.001
hohwille Dec 1, 2025
b42b543
Merge branch 'main' into feature/1166-automatic-project-import-for-in…
hohwille Dec 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE

== 2025.12.001

* https://github.com/devonfw/IDEasy/issues/1166[#1166]: Automatic project import for IntelliJ

Release with new features and bugfixes:

* https://github.com/devonfw/IDEasy/issues/39[#39]: Implement ToolCommandlet for pip
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.devonfw.tools.ide.environment;

import java.util.HashMap;
import java.util.Map;

import com.devonfw.tools.ide.context.IdeContext;

/**
* Subclass of {@link EnvironmentVariablesMap} that resolves variables recursively and allows new variables to be added to the resolver.
*/
public class ExtensibleEnvironmentVariables extends EnvironmentVariablesMap {

private final Map<String, String> additionalEnvironmentVariables;

/**
* The constructor.
*
* @param parent the parent {@link EnvironmentVariables} to inherit from.
* @param context the context to use.
*/
public ExtensibleEnvironmentVariables(AbstractEnvironmentVariables parent, IdeContext context) {
super(parent, context);
this.additionalEnvironmentVariables = new HashMap<>();
}

/**
* @param name the variable string to be resolved
* @param value the string the variable should be resolved into
*/
public void addVariableResolver(String name, String value) {
this.additionalEnvironmentVariables.put(name, value);
}

@Override
protected String getValue(String name, boolean ignoreDefaultValue) {
String value = this.additionalEnvironmentVariables.get(name);
if (value != null) {
return value;
}
return super.getValue(name, ignoreDefaultValue);
}

@Override
protected Map<String, String> getVariables() {
return this.additionalEnvironmentVariables;
}

@Override
public EnvironmentVariablesType getType() {
return EnvironmentVariablesType.TOOL;
}
}
28 changes: 23 additions & 5 deletions cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.nio.file.attribute.PosixFilePermission;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.function.Consumer;
Expand Down Expand Up @@ -154,8 +155,8 @@ default void copy(Path source, Path target) {
/**
* @param source the source {@link Path file or folder} to copy.
* @param target the {@link Path} to copy {@code source} to. Unlike the Linux {@code cp} command this method will not take the filename of {@code source}
* and copy that to {@code target} in case that is an existing folder. Instead it will always be simple and stupid and just copy from {@code source} to
* {@code target}. Therefore the result is always clear and easy to predict and understand. Also you can easily rename a file to copy. While
* and copy that to {@code target} in case that is an existing folder. Instead, it will always be simple and stupid and just copy from {@code source} to
* {@code target}. Therefore, the result is always clear and easy to predict and understand. Also, you can easily rename a file to copy. While
* {@code cp my-file target} may lead to a different result than {@code cp my-file target/} this method will always ensure that in the end you will find
* the same content of {@code source} in {@code target}.
* @param mode the {@link FileCopyMode}.
Expand All @@ -168,8 +169,8 @@ default void copy(Path source, Path target, FileCopyMode mode) {
/**
* @param source the source {@link Path file or folder} to copy.
* @param target the {@link Path} to copy {@code source} to. Unlike the Linux {@code cp} command this method will not take the filename of {@code source}
* and copy that to {@code target} in case that is an existing folder. Instead it will always be simple and stupid and just copy from {@code source} to
* {@code target}. Therefore the result is always clear and easy to predict and understand. Also you can easily rename a file to copy. While
* and copy that to {@code target} in case that is an existing folder. Instead, it will always be simple and stupid and just copy from {@code source} to
* {@code target}. Therefore, the result is always clear and easy to predict and understand. Also, you can easily rename a file to copy. While
* {@code cp my-file target} may lead to a different result than {@code cp my-file target/} this method will always ensure that in the end you will find
* the same content of {@code source} in {@code target}.
* @param mode the {@link FileCopyMode}.
Expand Down Expand Up @@ -331,6 +332,23 @@ default void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtrac
*/
Path findFirst(Path dir, Predicate<Path> filter, boolean recursive);

/**
* Searches upward from the given starting path to find the nearest ancestor directory that contains a specific subfolder. The search stops before ascending
* into any parent directory whose name matches the provided stop boundary.
*
* @param start the starting {@link Path} from which to begin the upward traversal. Must not be {@code null}.
* @param folderName the name of the subfolder to look for in each ancestor directory (e.g., ".idea"). Must not be {@code null} or empty.
* @param stopBeforeParentName the name of a parent directory at which the search should stop (case-insensitive). The method will not ascend into this
* directory. For example, if this is "workspaces", the search will stop at the child of "workspaces" and will not check inside "workspaces" itself.
* @return {@link Path} of the ancestor directory that contains the specified subfolder, or {@link Optional#empty()} if no such ancestor is found before
* reaching the stop boundary.
*/
Path findAncestorWithFolder(
Path start,
String folderName,
String stopBeforeParentName
);

/**
* @param dir the {@link Path} to the directory where to list the children.
* @param filter the {@link Predicate} used to {@link Predicate#test(Object) decide} which children to include (if {@code true} is returned).
Expand Down Expand Up @@ -510,7 +528,7 @@ default void writeProperties(Properties properties, Path file) {
/**
* @param properties the {@link Properties} to save.
* @param file the {@link Path} to the file where to save the properties.
* @param createParentDir if {@code true}, the parent directory will created if it does not already exist, {@code false} otherwise (fail if parent does
* @param createParentDir if {@code true}, the parent directory will be created if it does not already exist, {@code false} otherwise (fail if parent does
* not exist).
*/
void writeProperties(Properties properties, Path file, boolean createParentDir);
Expand Down
33 changes: 33 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,39 @@ private Path findFirstRecursive(Path dir, Predicate<Path> filter, boolean recurs
return null;
}

@Override
public Path findAncestorWithFolder(
Path start,
String folderName,
String stopBeforeParentName
) {

Path current = start.toAbsolutePath().normalize();

while (current != null) {

Path candidate = current.resolve(folderName);
if (Files.isDirectory(candidate)) {
return current;
}

Path parent = current.getParent();
if (parent == null) {
break;
}

Path parentName = parent.getFileName();
if (parentName != null &&
parentName.toString().equalsIgnoreCase(stopBeforeParentName)) {
break;
}

// Ascend
current = parent;
}
return null;
}

@Override
public List<Path> listChildrenMapped(Path dir, Function<Path, Path> filter) {

Expand Down
17 changes: 15 additions & 2 deletions cli/src/main/java/com/devonfw/tools/ide/merge/xml/XmlMerger.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ protected void doMerge(Path setup, Path update, EnvironmentVariables resolver, P
public Document merge(XmlMergeDocument templateDocument, XmlMergeDocument workspaceDocument, boolean workspaceFileExists) {

Document resultDocument;
Path source = templateDocument.getPath();
Path template = workspaceDocument.getPath();
Path template = templateDocument.getPath();
Path source = workspaceDocument.getPath();
this.context.debug("Merging {} into {} ...", template, source);
Element templateRoot = templateDocument.getRoot();
QName templateQName = XmlMergeSupport.getQualifiedName(templateRoot);
Expand Down Expand Up @@ -185,6 +185,19 @@ public XmlMergeDocument load(Path file) {
}
}

/**
* Creates an empty but valid xml file.
*
* @param tagName name of the tag to use for the root node.
* @param file Path of the file to create.
*/
public void createValidEmptyXmlFile(String tagName, Path file) {
Document document = DOCUMENT_BUILDER.newDocument();
Element root = document.createElement(tagName);
document.appendChild(root);
save(new XmlMergeDocument(document, file));
}

/**
* @param document the XML {@link XmlMergeDocument} to save.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
*/
public class Gradle extends LocalToolCommandlet {

private static final String BUILD_GRADLE = "build.gradle";
private static final String BUILD_GRADLE_KTS = "build.gradle.kts";
/** build.gradle file name */
public static final String BUILD_GRADLE = "build.gradle";

/** build.gradle.kts file name */
public static final String BUILD_GRADLE_KTS = "build.gradle.kts";
private static final String GRADLE_WRAPPER_FILENAME = "gradlew";

/**
Expand Down
102 changes: 100 additions & 2 deletions cli/src/main/java/com/devonfw/tools/ide/tool/intellij/Intellij.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
package com.devonfw.tools.ide.tool.intellij;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;

import org.w3c.dom.Document;

import com.devonfw.tools.ide.common.Tag;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
import com.devonfw.tools.ide.environment.EnvironmentVariables;
import com.devonfw.tools.ide.environment.ExtensibleEnvironmentVariables;
import com.devonfw.tools.ide.io.FileAccess;
import com.devonfw.tools.ide.io.FileAccessImpl;
import com.devonfw.tools.ide.merge.xml.XmlMergeDocument;
import com.devonfw.tools.ide.merge.xml.XmlMerger;
import com.devonfw.tools.ide.process.EnvironmentContext;
import com.devonfw.tools.ide.tool.ToolInstallation;
import com.devonfw.tools.ide.tool.gradle.Gradle;
import com.devonfw.tools.ide.tool.ide.IdeToolCommandlet;
import com.devonfw.tools.ide.tool.ide.IdeaBasedIdeToolCommandlet;
import com.devonfw.tools.ide.tool.mvn.Mvn;

/**
* {@link IdeToolCommandlet} for <a href="https://www.jetbrains.com/idea/">IntelliJ</a>.
Expand All @@ -21,6 +34,11 @@ public class Intellij extends IdeaBasedIdeToolCommandlet {

private static final String IDEA_BASH_SCRIPT = IDEA + ".sh";

private static final String TEMPLATE_LOCATION = "intellij/workspace/repository/.idea";
private static final String GRADLE_XML = "gradle.xml";
private static final String MISC_XML = "misc.xml";
private static final String IDEA_PROPERTIES = "idea.properties";

/**
* The constructor.
*
Expand Down Expand Up @@ -49,9 +67,89 @@ protected String getBinaryName() {

@Override
public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean additionalInstallation) {

super.setEnvironment(environmentContext, toolInstallation, additionalInstallation);
environmentContext.withEnvVar("IDEA_PROPERTIES", this.context.getWorkspacePath().resolve("idea.properties").toString());
environmentContext.withEnvVar("IDEA_PROPERTIES", this.context.getWorkspacePath().resolve(IDEA_PROPERTIES).toString());
}

private EnvironmentVariables getIntellijEnvironmentVariables(Path projectPath) {
ExtensibleEnvironmentVariables environmentVariables = new ExtensibleEnvironmentVariables(
(AbstractEnvironmentVariables) this.context.getVariables().getParent(), this.context);

environmentVariables.addVariableResolver("PROJECT_PATH", projectPath.toString());
return environmentVariables.resolved();
}

private void mergeConfig(Path repositoryPath, String configName) throws IOException {
Path workspacePath = getOrCreateWorkspaceXmlFile(repositoryPath, configName);

if (workspacePath != null) {
XmlMerger xmlMerger = new XmlMerger(context);
Path templatePath = this.context.getSettingsPath().resolve(TEMPLATE_LOCATION).resolve(configName);

EnvironmentVariables environmentVariables = getIntellijEnvironmentVariables(repositoryPath.getFileName());

if (!Files.exists(workspacePath)) {
xmlMerger.createValidEmptyXmlFile("project", workspacePath);
}
XmlMergeDocument workspaceDocument = xmlMerger.load(workspacePath);
XmlMergeDocument templateDocument = xmlMerger.loadAndResolve(templatePath, environmentVariables);

Document mergedDocument = xmlMerger.merge(templateDocument, workspaceDocument, false);

xmlMerger.save(mergedDocument, workspacePath);
}
}

private Path getOrCreateWorkspaceXmlFile(Path repositoryPath, String fileName) {
FileAccess fileAccess = new FileAccessImpl(context);

Path ideaParentPath = fileAccess
.findAncestorWithFolder(repositoryPath, "." + IDEA, "workspaces");

if (ideaParentPath != null) {
return ideaParentPath.resolve("." + IDEA).resolve(fileName);
}
return null;
}

private boolean importTemplatesExist() {
Path templatePath = this.context.getSettingsPath().resolve(TEMPLATE_LOCATION);
Path miscXml = templatePath.resolve(MISC_XML);
Path gradleXml = templatePath.resolve(GRADLE_XML);
return Files.exists(miscXml) && Files.exists(gradleXml);
}

@Override
public void importRepository(Path repositoryPath) {
if (!importTemplatesExist()) {
this.context.warning("Could not automatically import repository due to missing template files.");
return;
}
// check if pom.xml exists
Path pomPath = repositoryPath.resolve(Mvn.POM_XML);
if (Files.exists(pomPath)) {
try {
mergeConfig(repositoryPath, MISC_XML);
} catch (IOException e) {
this.context.error(e);
}

} else {
this.context.debug("no pom.xml found was found in {}", pomPath);
}

// check if build.gradle exists
Path javaGradlePath = repositoryPath.resolve(Gradle.BUILD_GRADLE);
Path kotlinGradlePath = repositoryPath.resolve(Gradle.BUILD_GRADLE_KTS);
if (Files.exists(javaGradlePath) || Files.exists(kotlinGradlePath)) {
try {
mergeConfig(repositoryPath, GRADLE_XML);
} catch (IOException e) {
this.context.error(e);
}
} else {
this.context.debug("no build.gradle found in {} and {}", javaGradlePath, kotlinGradlePath);
}
}

}
Loading