Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a122556
WIP Commit
areinicke Apr 27, 2026
c497948
Update logic
areinicke Apr 28, 2026
54a1fed
Removed --code option & Added further functionality
areinicke Apr 30, 2026
90e43e7
Fix tests
areinicke Apr 30, 2026
5dba7b5
Merge branch 'main' into feature/1695-clone-settings-to-temp-dir-for-…
areinicke Apr 30, 2026
aa751ff
Update comments
areinicke Apr 30, 2026
b19c6eb
Merge branch 'feature/1695-clone-settings-to-temp-dir-for-verificatio…
areinicke Apr 30, 2026
3e936dc
formatting cleanup
areinicke Apr 30, 2026
9bee5f1
Minor code refactor
areinicke Apr 30, 2026
29f6d1f
Update comments
areinicke Apr 30, 2026
8b69339
Update changelog
areinicke Apr 30, 2026
d1bfbf5
Add test case for invalid repository
areinicke Apr 30, 2026
64f1220
Replace hard coded variables
areinicke Apr 30, 2026
462d6d5
Fix tests
areinicke Apr 30, 2026
373fc19
Apply suggestion from @satorus
areinicke Apr 30, 2026
2552b4d
Move long if checks to own functions
areinicke Apr 30, 2026
d32fc4b
changed variable name to adhere to coding conventions
areinicke Apr 30, 2026
154ff6c
renamed method to follow coding conventions
areinicke Apr 30, 2026
6ce1e46
Merge branch 'main' into feature/1695-clone-settings-to-temp-dir-for-…
hohwille May 17, 2026
fcc2a0b
Step 1 Refactor
areinicke May 19, 2026
900c0f2
Merge branch 'main' into feature/1695-clone-settings-to-temp-dir-for-…
hohwille May 22, 2026
45d35be
Finish Step 1 Refactor
areinicke May 26, 2026
9cfd047
Merge branch 'feature/1695-clone-settings-to-temp-dir-for-verificatio…
areinicke May 26, 2026
0e82a26
Fixed temp location not being fully empty after projectr creation
areinicke May 26, 2026
777b997
Removed references to --code option in project creation
areinicke May 28, 2026
b5e23a7
Merge branch 'main' into feature/1695-clone-settings-to-temp-dir-for-…
areinicke Jul 7, 2026
36018b1
Remove ProjectNameConvention reference
areinicke Jul 7, 2026
94c6511
Merge branch 'main' into feature/1695-clone-settings-to-temp-dir-for-…
hohwille Jul 20, 2026
6320067
#1695: applied suggestions by @hohwille from original PR #1878
laim2003 Aug 3, 2026
6bf858a
Merge branch 'main' into feature/1695-clone-settings-to-temp-dir-for-…
laim2003 Aug 3, 2026
1c92338
#1695: added check for potential folder colision in CreateCommandlet
laim2003 Aug 3, 2026
43ac0f2
#1695: adapted tests to new project health checks
laim2003 Aug 3, 2026
351e43a
#1695: merged orginal changes by @areinicke into GitContextMock
laim2003 Aug 3, 2026
f413ed3
#1695: maven spotless plugin applied
laim2003 Aug 3, 2026
302a10e
Merge branch 'main' into feature/1695-clone-settings-to-temp-dir-for-…
laim2003 Aug 4, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ Release with new features and bugfixes:
* https://github.com/devonfw/IDEasy/issues/1788[#1788]: Add Commandlet to create links
* https://github.com/devonfw/IDEasy/issues/797[#797]: Use system unzip on macOS to preserve symlinks in ZIP extraction
* https://github.com/devonfw/IDEasy/issues/1723[#1723]: Add commandlet for GitHub Copilot CLI
* https://github.com/devonfw/IDEasy/issues/1695[#1695]: Clone settings to temporary directory, analyse, and then move

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move up

* https://github.com/devonfw/IDEasy/issues/1880[#1880]: Reinstall all plugins for IDE in force mode
* https://github.com/devonfw/IDEasy/issues/861[#861]: Fix install of pgadmin throws IllegalStateException when the install wizard starts
* https://github.com/devonfw/IDEasy/issues/1844[#1844]: VSCode plugin installation progress freezing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.devonfw.tools.ide.cli.CliException;
import com.devonfw.tools.ide.context.AbstractIdeContext;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.context.IdeStartContextImpl;
Expand Down Expand Up @@ -192,7 +191,6 @@ private void updateSettingsInStep(boolean codeRepository) {
this.context.getFileAccess().backup(settingsPath);
}
GitUrl gitUrl = getOrAskSettingsUrl();
checkProjectNameConvention(gitUrl.getProjectName());
initializeRepository(gitUrl);
return;
}
Expand All @@ -210,17 +208,10 @@ private GitUrl getOrAskSettingsUrl() {

String repository = this.settingsRepo.getValue();
repository = handleDefaultRepository(repository);
String userPromt;
String defaultUrl;
if (isCodeRepository()) {
userPromt = "Code repository URL:";
defaultUrl = null;
LOG.info(MESSAGE_CODE_REPO_URL);
} else {
userPromt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:";
defaultUrl = IdeContext.DEFAULT_SETTINGS_REPO_URL;
LOG.info(MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath());
}
String userPromt = "Repository URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:";
String defaultUrl = IdeContext.DEFAULT_SETTINGS_REPO_URL;
LOG.info(MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath());

GitUrl gitUrl = null;
if (repository != null) {
gitUrl = GitUrl.of(repository);
Expand All @@ -238,57 +229,18 @@ private GitUrl getOrAskSettingsUrl() {

private String handleDefaultRepository(String repository) {
if ("-".equals(repository)) {
if (isCodeRepository()) {
LOG.warn("'-' is found after '--code'. This is invalid.");
repository = null;
} else {
LOG.info("'-' was found for settings repository, the default settings repository '{}' will be used.", IdeContext.DEFAULT_SETTINGS_REPO_URL);
repository = IdeContext.DEFAULT_SETTINGS_REPO_URL;
}
LOG.info("'-' was found for the repository, the default settings repository '{}' will be used.", IdeContext.DEFAULT_SETTINGS_REPO_URL);
repository = IdeContext.DEFAULT_SETTINGS_REPO_URL;
}
return repository;
}

private void checkProjectNameConvention(String projectName) {
boolean isSettingsRepo = projectName.contains(IdeContext.SETTINGS_REPOSITORY_KEYWORD);
boolean codeRepository = isCodeRepository();
if (isSettingsRepo == codeRepository) {
String warningTemplate;
if (codeRepository) {
warningTemplate = """
Your git URL is pointing to the project name {} that contains the keyword '{}'.
Therefore we assume that you did a mistake by adding the '--code' option to the ide project creation.
Do you really want to create the project?""";
} else {
warningTemplate = """
Your git URL is pointing to the project name {} that does not contain the keyword ''{}''.
Therefore we assume that you forgot to add the '--code' option to the ide project creation.
Do you really want to create the project?""";
}
this.context.askToContinue(warningTemplate, projectName, IdeContext.SETTINGS_REPOSITORY_KEYWORD);
}
}

private void initializeRepository(GitUrl gitUrl) {

GitContext gitContext = this.context.getGitContext();
Path settingsPath = this.context.getSettingsPath();
Path repoPath = settingsPath;
boolean codeRepository = isCodeRepository();
if (codeRepository) {
// clone the given code repository into IDE_HOME/workspaces/main
repoPath = context.getWorkspacePath().resolve(gitUrl.getProjectName());
}
gitContext.pullOrClone(gitUrl, repoPath);
if (codeRepository) {
// check for settings folder and create symlink to IDE_HOME/settings
Path settingsFolder = repoPath.resolve(IdeContext.FOLDER_SETTINGS);
if (Files.exists(settingsFolder)) {
context.getFileAccess().symlink(settingsFolder, settingsPath);
} else {
throw new CliException("Invalid code repository " + gitUrl + ": missing a settings folder at " + settingsFolder);
}
}
this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath());
}

Expand Down Expand Up @@ -446,14 +398,4 @@ private void createStartScript(String ide, String workspace) {
fileAccess.writeFileContent(scriptContent, scriptPath);
fileAccess.makeExecutable(scriptPath);
}

/**
* Judge if the repository is a code repository.
*
* @return true when the repository is a code repository, otherwise false.
*/
protected boolean isCodeRepository() {
return false;
}

}
116 changes: 104 additions & 12 deletions cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.function.Predicate;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.devonfw.tools.ide.cli.CliException;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.environment.EnvironmentVariables;
import com.devonfw.tools.ide.git.GitUrl;
import com.devonfw.tools.ide.io.FileAccess;
import com.devonfw.tools.ide.log.IdeLogLevel;
import com.devonfw.tools.ide.property.FlagProperty;
import com.devonfw.tools.ide.property.StringProperty;
import com.devonfw.tools.ide.version.IdeVersion;

Expand All @@ -24,9 +27,6 @@ public class CreateCommandlet extends AbstractUpdateCommandlet {
/** {@link StringProperty} for the name of the new project */
public final StringProperty newProject;

/** {@link FlagProperty} for creating a project with settings inside a code repository */
public final FlagProperty codeRepositoryFlag;

/**
* The constructor.
*
Expand All @@ -36,7 +36,6 @@ public CreateCommandlet(IdeContext context) {

super(context);
this.newProject = add(new StringProperty("", true, "project"));
this.codeRepositoryFlag = add(new FlagProperty("--code"));
add(this.settingsRepo);
}

Expand All @@ -57,16 +56,24 @@ protected void doRun() {

String newProjectName = this.newProject.getValue();
Path newProjectPath = this.context.getIdeRoot().resolve(newProjectName);
Path tempProjectPath = this.context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS).resolve(newProjectName);

if (Files.exists(tempProjectPath)) {
throw new CliException(
String.format("Temporary project directory already exists in: %s. Please delete it and try again.", tempProjectPath));
} else if (Files.exists(newProjectPath)) {
throw new CliException(
String.format("Project directory already exists in: %s. As the project already exists, try calling 'ide update'.",
newProjectPath));
}
Comment on lines +61 to +68

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To fail from the start if the final location (newProjectPath) already exists, make somehow sense.
However, keep in mind that before this PR the user got a warning and was asked if he wants to continue.
IMHO this new approach is also valid and we can skip the old behaviour.
This also makes it easier since before that was easy possible since we were adding files to the final project directory and now we create a new project in tmp and finally move it - so if the final project director already exists moving would not work...

However, regarding the existence of tempProjectPath I would simply use FileAccess.backup to get rid of it without further notice. This is in a temp directory so we should not fail, lets just get rid of it and start from scratch or do you fear that the same user by accident is calling two ide create commands for the same project? That would IMHO be stupidity of the end-user...


LOG.info("Creating new IDEasy project in {}", newProjectPath);
if (!this.context.getFileAccess().isEmptyDir(newProjectPath)) {
this.context.askToContinue("Directory {} already exists. Do you want to continue?", newProjectPath);
Comment on lines 71 to 72

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you throw an exception for this above this does not make sense any more.

} else {
this.context.getFileAccess().mkdirs(newProjectPath);
}

initializeProject(newProjectPath);
this.context.setIdeHome(newProjectPath);
initializeProject(tempProjectPath);
this.context.setIdeHome(tempProjectPath);
super.doRun();
this.context.getFileAccess().writeFileContent(IdeVersion.getVersionString(), newProjectPath.resolve(IdeContext.FILE_SOFTWARE_VERSION));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I would expect the move from tempProjectPath to newProjectPath.

IdeLogLevel.SUCCESS.log(LOG, "Successfully created new project '{}'.", newProjectName);
Expand All @@ -83,14 +90,99 @@ private void initializeProject(Path newInstancePath) {
}

@Override
protected boolean isCodeRepository() {
return this.codeRepositoryFlag.isTrue();
protected void updateSettings() {
super.updateSettings();
analyzeProject();
}

/**
* This method is invoked when a new porject is created. It analyzes the cloned repository to check if it is a valid IDEasy repository. The repository can
* either be a settings repository (with ide.properties or devon.properties on the top level) or a code repository (with a settings folder on the top level
* containing such a file). Otherwise, the project creation fails and an error message is logged.
*/
private void analyzeProject() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method name is odd: As described in JavaDoc and what can be seen in the implementation, it does not "analyse the project" but "analyse the settings and potentially restructure them".
I would not expect a method with this name to do changes as side effect.
I would suggest to rename it to analyzeAndRestructureSettings().
Also this method should be moved to AbstractUpdateCommandlet since also the ide update command can repair the project so if settings folder is missing, it will ask for settings URL and clone the settings again. In that case we should invoke the same logic.

This method should however only move and restructure the settings to follow SoC and NOT move the entire project what IMHO belongs to doRun method that already has the tempProjectPath and newProjectPath variables and the responsibility of handling the tmp creation (and therefore also the according cleanup).

// Settings repository: ide.properties on top levels (or devon.properties for legacy users)
// Code repository: settings folder on top level with ide.properties inside (or devon.properties for legacy users)
String projectName = this.context.getProjectName();
Path actualProjectPath = this.context.getIdeRoot().resolve(projectName);
FileAccess fileAccess = this.context.getFileAccess();
Path settingsPath = this.context.getSettingsPath();

// Check whether the repository is a valid settings repository, code repository, or neither
if (isSettingsRepository(settingsPath)) {
LOG.info("The repository seems to be a settings repository based on the presence of " + EnvironmentVariables.DEFAULT_PROPERTIES + " or "
+ EnvironmentVariables.LEGACY_PROPERTIES + " on the top level.");
moveProject(this.context.getIdeHome(), actualProjectPath);

} else if (isCodeRepository(settingsPath)) {
LOG.info(EnvironmentVariables.DEFAULT_PROPERTIES + " or " + EnvironmentVariables.LEGACY_PROPERTIES
+ " found in settings subfolder. This indicates a code repository with a settings folder on the top level.");

String gitProjectName = GitUrl.of(this.settingsRepo.getValue(0)).getProjectName();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indexed access shall only be used for multi-valued properties.

Suggested change
String gitProjectName = GitUrl.of(this.settingsRepo.getValue(0)).getProjectName();
String gitProjectName = GitUrl.of(this.settingsRepo.getValue()).getProjectName();

Path codeFolderPath = actualProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN).resolve(gitProjectName);
// Move temp project to actual project location $IDE_ROOT/<project_name>
moveProject(this.context.getIdeHome(), actualProjectPath);

// Move settings fodler containing code to $IDE_ROOT/<project_name>/workspaces/main/<git_project_name>
moveProject(actualProjectPath.resolve(IdeContext.FOLDER_SETTINGS), codeFolderPath);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The settings are not a project.
Hence the method moveProject is named wrong.


// Set IDE_HOME to new (and actual) project location
this.context.setIdeHome(actualProjectPath);

// Link settings folder in IDE_HOME to settings folder in code repository
fileAccess.symlink(codeFolderPath.resolve(IdeContext.FOLDER_SETTINGS), actualProjectPath.resolve(IdeContext.FOLDER_SETTINGS));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one thought that I may have missed and that @areinicke never told me.
Is it possible that he did the moving of the project itself before this on purpose?
Assuming we are on Windows and we do not have permissions for proper symlinks, we will fallback to a junction - then IMHO this cannot be created relative.
In that case, we would create an absolute link here and following my review comments about SoC the resulting project would be broken if we move from tmp to projects after creating this absolute link.
Is that thought correct? Do we have a clean solution for this?
We have to keep in mind that we also need to solve and reuse this for the ide update command.
In the original PR we wanted to keep things simpler and after the massive review ping pongs and reworks, we decided to implement this in another go.
However, at least the design should keep that in mind.
Maybe it will not work out, if we do not implement and test these 2 aspects together...


} else {
// Repository seems to be invalid. Clean up temporary location and return error
fileAccess.delete(this.context.getIdeHome());
throw new CliException("This repository does not include an " + EnvironmentVariables.DEFAULT_PROPERTIES + " or " + EnvironmentVariables.LEGACY_PROPERTIES
+ " file at the top level or a settings folder with such a file. "
+ "The repository does not seem to be a valid IDEasy repository. Please verify the repository and try again.");
}
// Set IDE_HOME to new (and actual) project location
this.context.setIdeHome(actualProjectPath);
}

/**
* Moves files of a new projectfrom the temporary location to the final project location.
*
* @param oldPath - The path of the file or directory to be moved.
* @param newPath - The path of the destination.
*/
private void moveProject(Path oldPath, Path newPath) {
FileAccess fileAccess = this.context.getFileAccess();
try {
fileAccess.mkdirs(newPath);
fileAccess.move(oldPath, newPath, StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
LOG.error("Failed to move project from {} to {}. Please move it manually.", oldPath, newPath, e);
}
}
Comment on lines +152 to +160

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand the purpose of this method at all.

  1. Why do we do an mkdirs to the target directory if we replace it anyways? Is that because the parent directory may not exist? Shouldn't this be handled by FileAccess.move then?
  2. REPLACE_EXISTING sounds dangerous to me. Do we really want to delete a target project that may have been created in projects folder with the same name concurrently?
  3. The exception handling is crazy: If the move failed we log and error and continue reporting the project creation was successful what is a lie and the implicit icd to navigate to the newly created project will then fail? This is nuts.
  4. So if we do nothing else in the end then calling FileAccess.move why do we need a private method for that?I would simply remove this method.


/**
* Checks whether te given repository is a settings repository by checking for the presence of ide.properties or devon.properties on the top level.
*
* @param repositoryPath - The path of the repository to be checked.
*/
private boolean isSettingsRepository(Path repositoryPath) {
return Files.exists(repositoryPath.resolve(EnvironmentVariables.DEFAULT_PROPERTIES)) || Files.exists(
repositoryPath.resolve(EnvironmentVariables.LEGACY_PROPERTIES));
}

/**
* Checks whether te given repository is a code repository by checking for the presence of ide.properties or devon.properties within a settings folder on the
* top level.
*
* @param repositoryPath - The path of the repository to be checked.
*/
private boolean isCodeRepository(Path repositoryPath) {
return isSettingsRepository(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS));
}

@Override
protected String getStepMessage() {

return "Create (clone) " + (isCodeRepository() ? "code" : "settings") + " repository";
return "Create (Clone) repository";
}

private void logWelcomeMessage() {
Expand Down
1 change: 0 additions & 1 deletion cli/src/main/resources/nls/Help.properties
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,6 @@ cmd.yarn.detail=Yarn is a package manager and build tool for JavaScript. Detaile
commandlets=Available commandlets:
icd-hint=Hint: Use 'icd' command to easily navigate between your IDE home, projects, and workspaces. Type 'icd --help' for more details.
opt.--batch=enable batch mode (non-interactive).
opt.--code=clone given code repository containing a settings folder into workspaces so that settings can be committed alongside code changes.
opt.--debug=enable debug logging.
opt.--force=enable force mode.
opt.--force-plugin-reinstall=resets installed plugins to the project configuration
Expand Down
1 change: 0 additions & 1 deletion cli/src/main/resources/nls/Help_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,6 @@ cmd.yarn.detail=Yarn ist ein Package Manager und Build-Werkzeug für JavaScript.
commandlets=Verfügbare Kommandos:
icd-hint=Hinweis: Verwenden Sie den Befehl 'icd' um einfach zwischen Ihrem IDE-Hauptverzeichnis, Projekten und Workspaces zu navigieren. Geben Sie 'icd --help' für weitere Details ein.
opt.--batch=Aktiviert den Batch-Modus (nicht-interaktive Stapelverarbeitung).
opt.--code=Git-Repository sowohl als Code- als auch als Settings-Repository verwenden.
opt.--debug=Aktiviert Debug-Ausgaben (Fehleranalyse).
opt.--force=Aktiviert den Force-Modus (Erzwingen).
opt.--force-plugin-reinstall=Setzt installierte Plugins zurück auf die Projektkonfiguration.
Expand Down
Loading
Loading