From 27edd9341d0e2cd11ecec240bbea291d6859dcbc Mon Sep 17 00:00:00 2001 From: laim2003 Date: Thu, 13 Aug 2026 16:10:14 +0200 Subject: [PATCH 01/89] #1695: initial commit Signed-off-by: laim2003 --- .../commandlet/AbstractUpdateCommandlet.java | 65 ++++++++++++++++++- .../ide/git/repository/RepositoryUtil.java | 53 +++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java index 2874a0ef04..a6061a293e 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java @@ -19,6 +19,7 @@ import com.devonfw.tools.ide.git.GitContext; import com.devonfw.tools.ide.git.GitUrl; import com.devonfw.tools.ide.git.repository.RepositoryCommandlet; +import com.devonfw.tools.ide.git.repository.RepositoryUtil; import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.property.FlagProperty; import com.devonfw.tools.ide.property.StringProperty; @@ -182,6 +183,7 @@ private void updateSettingsInStep(boolean codeRepository) { if (!settingsRepository) { if (Files.exists(settingsPath)) { if (!this.context.getFileAccess().isEmptyDir(settingsPath)) { + // settings folder seems to be invalid this.context.askToContinue( "Your settings repository seems to be broken ('.git' folder not present). " + "We can fix this by moving your settings the backed up. " @@ -191,9 +193,10 @@ private void updateSettingsInStep(boolean codeRepository) { } this.context.getFileAccess().backup(settingsPath); } + //settings folder does not exist (yet), lets retrieve the settings url to pull GitUrl gitUrl = getOrAskSettingsUrl(); checkProjectNameConvention(gitUrl.getProjectName()); - initializeRepository(gitUrl); + pullAndVerify(gitUrl); return; } } @@ -236,6 +239,64 @@ private GitUrl getOrAskSettingsUrl() { return gitUrl; } + /** + * We pull the settings repo from the remote into a temporary folder to perform health checks. + */ + private void pullAndVerify(GitUrl gitUrl) { + GitContext gitContext = this.context.getGitContext(); + Path tempProjectPath = this.context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS).resolve(this.context.getProjectName()); + + gitContext.pullOrClone(gitUrl, tempProjectPath); + + checkIntegrityAndMove(tempProjectPath, gitUrl.getProjectName()); + } + + private void checkIntegrityAndMove(Path projectPath, String gitProjectName) { + + FileAccess fileAccess = this.context.getFileAccess(); + + if (!Files.exists(projectPath)) { + throw new CliException(getIntegrityCheckErrorMessage("Git pull target folder does not exist.")); + } + + Path finalSettingsPath; + switch (RepositoryUtil.getRepositoryType(projectPath, gitProjectName)) { + case CODE -> { + + finalSettingsPath = this.context.getIdeHome().resolve(this.context.getProjectName()).resolve(IdeContext.FOLDER_SETTINGS); + moveProject(projectPath, finalSettingsPath); + } + case SETTINGS -> { + + finalSettingsPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); + moveProject(projectPath, finalSettingsPath); + } + case CODE_SETTINGS_COMBINED -> { + + finalSettingsPath = this.context.getWorkspacePath().resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS); + Path symLinkLocation = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); + + } + case UNKNOWN -> + throw new CliException(getIntegrityCheckErrorMessage("The specified repository could not be validated as either a code or settings repository.")); + } + } + + private Path moveProject(Path from, Path to) { + + FileAccess fileAccess = this.context.getFileAccess(); + try { + fileAccess.move(from, to); + } catch (Exception e) { + throw new CliException(getIntegrityCheckErrorMessage(String.format("Failed to move project from %s to %s", from, to)), e); + } + return to; + } + + private String getIntegrityCheckErrorMessage(String message) { + return String.format("Settings repository integrity check failed: %s", message); + } + private String handleDefaultRepository(String repository) { if ("-".equals(repository)) { if (isCodeRepository()) { @@ -275,7 +336,7 @@ private void initializeRepository(GitUrl gitUrl) { Path settingsPath = this.context.getSettingsPath(); Path repoPath = settingsPath; boolean codeRepository = isCodeRepository(); - if (codeRepository) { + if (codeRepository) { //this never gets executed because isCodeRepository is always false // clone the given code repository into IDE_HOME/workspaces/main repoPath = context.getWorkspacePath().resolve(gitUrl.getProjectName()); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java new file mode 100644 index 0000000000..66db4d1b4a --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -0,0 +1,53 @@ +package com.devonfw.tools.ide.git.repository; + +import java.nio.file.Files; +import java.nio.file.Path; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.environment.EnvironmentVariables; + +/// Utility class for IDEasy settings/code repositories +public class RepositoryUtil { + + /** + * Checks whether te given repository is a settings repository, a combined settings and code repository, or a typical code repositor. Combined code and + * settings repository is detected by checking whether IDE_HOME/workspaces/main/[gitProjectName]/settings exists and is a valid settings repository. + * + * @param repositoryPath - The path of the repository to be checked. + * @return {@link RepositoryType} of the repository. + */ + public static RepositoryType getRepositoryType(Path repositoryPath, String gitProjectName) { + + if (!Files.exists(repositoryPath)) { + return RepositoryType.UNKNOWN; + } + + if (Files.exists(repositoryPath.resolve(EnvironmentVariables.DEFAULT_PROPERTIES)) + || Files.exists(repositoryPath.resolve(EnvironmentVariables.LEGACY_PROPERTIES))) { + return RepositoryType.SETTINGS; + } else if (gitProjectName != null + && Files.exists( + repositoryPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN).resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS)) + && getRepositoryType( + repositoryPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN).resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS), + gitProjectName) == RepositoryType.SETTINGS) { + return RepositoryType.CODE_SETTINGS_COMBINED; + } else if (!Files.isSymbolicLink(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS)) + && getRepositoryType(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), gitProjectName) == RepositoryType.SETTINGS) { + return RepositoryType.CODE; + } + return RepositoryType.UNKNOWN; + } + + /// enum representation of a detected {@link RepositoryType} + public enum RepositoryType { + /// Git Repository is a code repository. + CODE, + /// Git Repository is a settings repository. + SETTINGS, + /// A combined code & settings repository contains both the settings-folder and the code within the workspace folder. + CODE_SETTINGS_COMBINED, + /// The type of the repository could not be determined. + UNKNOWN + } +} From dc82ff1e0548bd8609871e9eee4ab8b8d22f7c42 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 15:29:55 +0200 Subject: [PATCH 02/89] #1695: removed code repository flag Signed-off-by: laim2003 --- .../tools/ide/commandlet/CreateCommandlet.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index a68d6768c5..ed2e937404 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -10,7 +10,6 @@ import com.devonfw.tools.ide.context.IdeContext; 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; @@ -24,9 +23,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. * @@ -36,7 +32,6 @@ public CreateCommandlet(IdeContext context) { super(context); this.newProject = add(new StringProperty("", true, "project")); - this.codeRepositoryFlag = add(new FlagProperty("--code")); add(this.settingsRepo); } @@ -82,15 +77,10 @@ private void initializeProject(Path newInstancePath) { fileAccess.mkdirs(newInstancePath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)); } - @Override - protected boolean isCodeRepository() { - return this.codeRepositoryFlag.isTrue(); - } - @Override protected String getStepMessage() { - return "Create (clone) " + (isCodeRepository() ? "code" : "settings") + " repository"; + return "Create (clone) repository"; } private void logWelcomeMessage() { From 9196d21ed3786bc729ff4f2eded3bb020b5feed3 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 15:30:30 +0200 Subject: [PATCH 03/89] #1695: Added new helper class to determine the type of a repository. Signed-off-by: laim2003 --- .../ide/git/repository/RepositoryUtil.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 66db4d1b4a..413f45552d 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -10,7 +10,7 @@ public class RepositoryUtil { /** - * Checks whether te given repository is a settings repository, a combined settings and code repository, or a typical code repositor. Combined code and + * Checks whether te given git repository is a settings repository, a combined settings and code repository, or a typical code repositor. Combined code and * settings repository is detected by checking whether IDE_HOME/workspaces/main/[gitProjectName]/settings exists and is a valid settings repository. * * @param repositoryPath - The path of the repository to be checked. @@ -27,13 +27,12 @@ public static RepositoryType getRepositoryType(Path repositoryPath, String gitPr return RepositoryType.SETTINGS; } else if (gitProjectName != null && Files.exists( - repositoryPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN).resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS)) + repositoryPath.resolve(IdeContext.FOLDER_SETTINGS)) && getRepositoryType( - repositoryPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN).resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS), + repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), gitProjectName) == RepositoryType.SETTINGS) { return RepositoryType.CODE_SETTINGS_COMBINED; - } else if (!Files.isSymbolicLink(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS)) - && getRepositoryType(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), gitProjectName) == RepositoryType.SETTINGS) { + } else if (!Files.exists(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS))) { return RepositoryType.CODE; } return RepositoryType.UNKNOWN; @@ -42,12 +41,15 @@ && getRepositoryType(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), gitProj /// enum representation of a detected {@link RepositoryType} public enum RepositoryType { /// Git Repository is a code repository. - CODE, + CODE("code"), /// Git Repository is a settings repository. - SETTINGS, + SETTINGS("settings"), /// A combined code & settings repository contains both the settings-folder and the code within the workspace folder. - CODE_SETTINGS_COMBINED, + CODE_SETTINGS_COMBINED("code & settings"), /// The type of the repository could not be determined. - UNKNOWN + UNKNOWN("unknown"); + + RepositoryType(String displayName) { + } } } From b7eafb984baeb028c49c589bd9c10d9eba128354 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 16:12:58 +0200 Subject: [PATCH 04/89] #1695: added logic that moves a settings directory to a temp dir, verifies its health and then moves it to the IDE_HOME Signed-off-by: laim2003 --- .../commandlet/AbstractUpdateCommandlet.java | 111 ++++-------------- 1 file changed, 25 insertions(+), 86 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java index a6061a293e..19a3319903 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java @@ -195,8 +195,7 @@ private void updateSettingsInStep(boolean codeRepository) { } //settings folder does not exist (yet), lets retrieve the settings url to pull GitUrl gitUrl = getOrAskSettingsUrl(); - checkProjectNameConvention(gitUrl.getProjectName()); - pullAndVerify(gitUrl); + pullAndCheckIntegrity(gitUrl); return; } } @@ -213,17 +212,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 = "Settings 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); @@ -242,7 +234,7 @@ private GitUrl getOrAskSettingsUrl() { /** * We pull the settings repo from the remote into a temporary folder to perform health checks. */ - private void pullAndVerify(GitUrl gitUrl) { + private void pullAndCheckIntegrity(GitUrl gitUrl) { GitContext gitContext = this.context.getGitContext(); Path tempProjectPath = this.context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS).resolve(this.context.getProjectName()); @@ -259,26 +251,31 @@ private void checkIntegrityAndMove(Path projectPath, String gitProjectName) { throw new CliException(getIntegrityCheckErrorMessage("Git pull target folder does not exist.")); } - Path finalSettingsPath; + Path targetDirectory; switch (RepositoryUtil.getRepositoryType(projectPath, gitProjectName)) { - case CODE -> { - - finalSettingsPath = this.context.getIdeHome().resolve(this.context.getProjectName()).resolve(IdeContext.FOLDER_SETTINGS); - moveProject(projectPath, finalSettingsPath); - } + case CODE -> throw new CliException( + getIntegrityCheckErrorMessage( + "The given git repository URL points to a code repository. The <> parameter only accepts a settings or a combined code-settings repository.")); case SETTINGS -> { - finalSettingsPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); - moveProject(projectPath, finalSettingsPath); + targetDirectory = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); + moveProject(projectPath, targetDirectory); + this.context.getGitContext().saveCurrentCommitId(targetDirectory, this.context.getSettingsCommitIdPath()); } case CODE_SETTINGS_COMBINED -> { - finalSettingsPath = this.context.getWorkspacePath().resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS); - Path symLinkLocation = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); + //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. (Formerly managed by the obsolete "--code" flag) + targetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); + moveProject(projectPath, targetDirectory); + + Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); + Path symlinkTargetPath = this.context.getWorkspacePath().resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS); + fileAccess.symlink(symlinkTargetPath, symlinkPath); + this.context.getGitContext().saveCurrentCommitId(symlinkTargetPath, this.context.getSettingsCommitIdPath()); } - case UNKNOWN -> - throw new CliException(getIntegrityCheckErrorMessage("The specified repository could not be validated as either a code or settings repository.")); + case UNKNOWN -> throw new CliException( + getIntegrityCheckErrorMessage("The specified repository could not be validated as either a code, settings or combined code-settings repository.")); } } @@ -299,60 +296,12 @@ private String getIntegrityCheckErrorMessage(String message) { 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 settings 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) { //this never gets executed because isCodeRepository is always false - // 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()); - } - private void updateSoftware() { if (this.skipTools.isTrue()) { @@ -507,14 +456,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; - } - } From fc1ca0902f165515b5eb25da515d681a07523419 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 16:14:04 +0200 Subject: [PATCH 05/89] #1695: fixed typo Signed-off-by: laim2003 --- .../tools/ide/commandlet/AbstractUpdateCommandlet.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java index 19a3319903..05954ddc20 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java @@ -212,7 +212,7 @@ private GitUrl getOrAskSettingsUrl() { String repository = this.settingsRepo.getValue(); repository = handleDefaultRepository(repository); - String userPromt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; + String userPrompt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; String defaultUrl = IdeContext.DEFAULT_SETTINGS_REPO_URL; LOG.info(MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath()); @@ -221,7 +221,7 @@ private GitUrl getOrAskSettingsUrl() { gitUrl = GitUrl.of(repository); } while ((gitUrl == null) || !gitUrl.isValid()) { - repository = this.context.askForInput(userPromt, defaultUrl); + repository = this.context.askForInput(userPrompt, defaultUrl); repository = handleDefaultRepository(repository); gitUrl = GitUrl.of(repository); if (!gitUrl.isValid()) { From dc5c8121268478ced53a685f0662065de2a05ae8 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 16:21:44 +0200 Subject: [PATCH 06/89] #1695: cleanup of RepositoryUtil Signed-off-by: laim2003 --- .../com/devonfw/tools/ide/git/repository/RepositoryUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 413f45552d..1b940397e8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -45,7 +45,7 @@ public enum RepositoryType { /// Git Repository is a settings repository. SETTINGS("settings"), /// A combined code & settings repository contains both the settings-folder and the code within the workspace folder. - CODE_SETTINGS_COMBINED("code & settings"), + CODE_SETTINGS_COMBINED("code & settings combined"), /// The type of the repository could not be determined. UNKNOWN("unknown"); From 6d4431562b24bdeb9293327a9fe479ce290d3626 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 16:28:52 +0200 Subject: [PATCH 07/89] #1695: updated CHANGELOG.adoc Signed-off-by: laim2003 --- CHANGELOG.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 8866a010de..042b50039f 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -5,7 +5,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE == 2026.08.002 Release with new features and bugfixes: - +* https://github.com/devonfw/IDEasy/issues/1695[#1695]: Project creation logic extended with health checks and removed `--code` flag. The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/49?closed=1[milestone 2026.08.002]. From ead50be9d5001ca04f63aab2e2b11bf6f77f6baf Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 16:29:13 +0200 Subject: [PATCH 08/89] #1695: cleanup of RepositoryUtil Signed-off-by: laim2003 --- .../tools/ide/git/repository/RepositoryUtil.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 1b940397e8..9a6be63c13 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -41,15 +41,12 @@ && getRepositoryType( /// enum representation of a detected {@link RepositoryType} public enum RepositoryType { /// Git Repository is a code repository. - CODE("code"), + CODE, /// Git Repository is a settings repository. - SETTINGS("settings"), + SETTINGS, /// A combined code & settings repository contains both the settings-folder and the code within the workspace folder. - CODE_SETTINGS_COMBINED("code & settings combined"), + CODE_SETTINGS_COMBINED, /// The type of the repository could not be determined. - UNKNOWN("unknown"); - - RepositoryType(String displayName) { - } + UNKNOWN } } From 4cd4abef12ab6219fdabbb979543d026c2077363 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 16:43:12 +0200 Subject: [PATCH 09/89] #1695: removed help description for --code flag Signed-off-by: laim2003 --- cli/src/main/resources/nls/Help.properties | 1 - cli/src/main/resources/nls/Help_de.properties | 1 - 2 files changed, 2 deletions(-) diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties index 6b1395ea84..7ca4652fac 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -180,7 +180,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 diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties index 2c1fb07b21..8059bfd605 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -180,7 +180,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. From 3c3ddf7d497713a0e5a797b5916beee51013c2c0 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 16:47:23 +0200 Subject: [PATCH 10/89] #1695: Updated documentation Signed-off-by: laim2003 --- documentation/settings.adoc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/documentation/settings.adoc b/documentation/settings.adoc index e9de2335ca..3d7a1540a1 100644 --- a/documentation/settings.adoc +++ b/documentation/settings.adoc @@ -18,17 +18,18 @@ This gives you the freedom to control and manage the tools with their versions a To setup and customize these settings simply follow the link:usage.adoc#admin[admin usage guide]. Then tell your team to create the project using your project sepcific settings git URL: ``` -ide create «project-name» --code «settings-url» +ide create «project-name» «settings-url» ``` == Code-repository It is even possible to include your settings into your code repository by having the `settings` folder directly on top-level of your code git repository. This allows you to keep settings changes in sync with code changes and manage them in the same pull/merge requests. -To use this approach simply copy the content of https://github.com/devonfw/ide-settings[ide-settings] to a top-level `settings` folder in your code repository root and tell your developers to create the project usining the `--code` option: +To use this approach simply copy the content of https://github.com/devonfw/ide-settings[ide-settings] to a top-level `settings` folder in your code repository root. +IDEasy will automatically recognize that you are using a code repository, therefore just use the same command as above: ``` -ide create «project-name» --code «code-repo-url» +ide create «project-name» «code-repo-url» ``` IDEasy will clone your repository and create a symlink to the settings folder. From 2a54e07c5b4cc5999d9282c6f66b2eb5f437d9aa Mon Sep 17 00:00:00 2001 From: laim2003 Date: Mon, 17 Aug 2026 14:46:13 +0200 Subject: [PATCH 11/89] #1695: small fixes Signed-off-by: laim2003 --- .../commandlet/AbstractUpdateCommandlet.java | 22 +++++++++---------- .../devonfw/tools/ide/git/GitContextMock.java | 3 +++ .../test/resources/settings/ide.properties | 0 3 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 cli/src/test/resources/settings/ide.properties diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java index 05954ddc20..094b30e7e8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java @@ -168,7 +168,7 @@ protected void updateSettings() { LOG.info("Skipping git pull in settings due to code repository. Use --force-pull to enforce pulling."); return; } - this.context.newStep(getStepMessage()).run(() -> updateSettingsInStep(codeRepository)); + this.context.newStep(getStepMessage()).run(() -> updateSettingsInStep(codeRepository), true); } protected String getStepMessage() { @@ -185,11 +185,8 @@ private void updateSettingsInStep(boolean codeRepository) { if (!this.context.getFileAccess().isEmptyDir(settingsPath)) { // settings folder seems to be invalid this.context.askToContinue( - "Your settings repository seems to be broken ('.git' folder not present). " - + "We can fix this by moving your settings the backed up. " - + "You will be asked for the settings git URL and your settings will be cloned from scratch. " - + "Do you want to proceed?" - ); + "Your settings repository seems to be broken ('.git' folder not present). " + "We can fix this by moving your settings the backed up. " + + "You will be asked for the settings git URL and your settings will be cloned from scratch. " + "Do you want to proceed?"); } this.context.getFileAccess().backup(settingsPath); } @@ -253,9 +250,6 @@ private void checkIntegrityAndMove(Path projectPath, String gitProjectName) { Path targetDirectory; switch (RepositoryUtil.getRepositoryType(projectPath, gitProjectName)) { - case CODE -> throw new CliException( - getIntegrityCheckErrorMessage( - "The given git repository URL points to a code repository. The <> parameter only accepts a settings or a combined code-settings repository.")); case SETTINGS -> { targetDirectory = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); @@ -274,8 +268,12 @@ private void checkIntegrityAndMove(Path projectPath, String gitProjectName) { fileAccess.symlink(symlinkTargetPath, symlinkPath); this.context.getGitContext().saveCurrentCommitId(symlinkTargetPath, this.context.getSettingsCommitIdPath()); } - case UNKNOWN -> throw new CliException( - getIntegrityCheckErrorMessage("The specified repository could not be validated as either a code, settings or combined code-settings repository.")); + default -> { + fileAccess.backup(projectPath); + throw new CliException(getIntegrityCheckErrorMessage(String.format( + "The given git repository URL does not point to a valid settings or code-settings repository. Please verify and try again. Before trying again, please delete the folder %s", + this.context.getIdeHome()))); + } } } @@ -296,7 +294,7 @@ private String getIntegrityCheckErrorMessage(String message) { private String handleDefaultRepository(String repository) { if ("-".equals(repository)) { - LOG.info("'-' was found for settings repository, the default settings repository '{}' will be used.", 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; diff --git a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java index 2f5c689834..e608dace12 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java @@ -53,6 +53,9 @@ public void clone(GitUrl gitUrl, Path repository) { FileAccess fileAccess = this.context.getFileAccess(); fileAccess.mkdirs(repository); + // Create ide.properties to simulate a valid repository + fileAccess.touch(repository.resolve("ide.properties")); + Path gitFolder = repository.resolve(GIT_FOLDER); fileAccess.mkdirs(gitFolder); String branch = gitUrl.branch(); diff --git a/cli/src/test/resources/settings/ide.properties b/cli/src/test/resources/settings/ide.properties new file mode 100644 index 0000000000..e69de29bb2 From 6e242918f2dcec84e3b00719e9863a4508e47101 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Mon, 17 Aug 2026 14:46:22 +0200 Subject: [PATCH 12/89] #1695: added tests Signed-off-by: laim2003 --- .../ide/commandlet/CreateCommandletTest.java | 76 ++++++------------- 1 file changed, 24 insertions(+), 52 deletions(-) diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java index 98534e72a9..06b3b5ca4c 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java @@ -7,15 +7,12 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import com.devonfw.tools.ide.cli.CliArguments; import com.devonfw.tools.ide.cli.CliException; import com.devonfw.tools.ide.context.AbstractIdeContextTest; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.context.IdeTestContext; -import com.devonfw.tools.ide.context.ProcessContextGitMock; import com.devonfw.tools.ide.environment.EnvironmentVariables; import com.devonfw.tools.ide.environment.EnvironmentVariablesType; import com.devonfw.tools.ide.git.GitContextImplMock; @@ -67,54 +64,7 @@ void testCreateCommandletRun() { assertThat(newProjectPath.resolve(IdeContext.FOLDER_PLUGINS)).exists(); assertThat(newProjectPath.resolve(IdeContext.FOLDER_SOFTWARE)).exists(); assertThat(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)).exists(); - } - - @ParameterizedTest - @ValueSource(strings = { "https://some-code-repository", "ssh://some-settings-repository" }) - void testWarningWhenRepoDoesNotMeetNamingConvention(String invalidRepo, @TempDir Path tempDir) { - // arrange - ProcessContextGitMock gitMock = new ProcessContextGitMock(context, tempDir); - context.setProcessContext(gitMock); - CreateCommandlet cc = context.getCommandletManager().getCommandlet(CreateCommandlet.class); - cc.newProject.setValueAsString(NEW_PROJECT_NAME, context); - cc.codeRepositoryFlag.setValue(!invalidRepo.contains("code")); // raise conflict - cc.settingsRepo.setValue(invalidRepo); - cc.skipTools.setValue(true); - context.setAnswers("yes"); - // act - cc.run(); - // assert - assertThat(context).logAtInteraction().hasMessageContaining("Do you really want to create the project?"); - Path newProjectPath = context.getIdeRoot().resolve(NEW_PROJECT_NAME); - assertThat(newProjectPath).exists(); - assertThat(context.getIdeHome()).isEqualTo(newProjectPath); - assertThat(newProjectPath.resolve(IdeContext.FOLDER_PLUGINS)).exists(); - assertThat(newProjectPath.resolve(IdeContext.FOLDER_SOFTWARE)).exists(); - assertThat(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)).exists(); - } - - @Test - void testWarningWhenCodeRepoUsingDefaultMark(@TempDir Path tempDir) { - String invalidCodeRepo = "-"; - // arrange - ProcessContextGitMock gitMock = new ProcessContextGitMock(context, tempDir); - context.setProcessContext(gitMock); - CreateCommandlet cc = context.getCommandletManager().getCommandlet(CreateCommandlet.class); - cc.newProject.setValueAsString(NEW_PROJECT_NAME, context); - cc.settingsRepo.setValue(invalidCodeRepo); - cc.codeRepositoryFlag.setValue(true); - cc.skipTools.setValue(true); - context.setAnswers("https://some-code-repository"); - // act - cc.run(); - // assert - assertThat(context).logAtWarning().hasMessageContaining("'-' is found after '--code'. This is invalid."); - Path newProjectPath = context.getIdeRoot().resolve(NEW_PROJECT_NAME); - assertThat(newProjectPath).exists(); - assertThat(context.getIdeHome()).isEqualTo(newProjectPath); - assertThat(newProjectPath.resolve(IdeContext.FOLDER_PLUGINS)).exists(); - assertThat(newProjectPath.resolve(IdeContext.FOLDER_SOFTWARE)).exists(); - assertThat(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)).exists(); + assertThat(context.getIdeRoot().resolve("_ide/tmp/projects").resolve(NEW_PROJECT_NAME)).doesNotExist(); } @Test @@ -220,6 +170,28 @@ void testWelcomeMessageDisplayed() { assertThat(context).logAtInfo().hasMessageContaining("Welcome to your new IDEasy project!"); } + @Test + void testProjectWithInvalidRepositoryNotCreated() { + + // arrange - create a new project that is invalid (does not contain ide.properties file) + GitContextImplMock gitContextImplMock = new GitContextImplMock(context, TEST_RESOURCES.resolve("pypi")); + + context.setGitContext(gitContextImplMock); + CreateCommandlet cc = context.getCommandletManager().getCommandlet(CreateCommandlet.class); + cc.newProject.setValueAsString(NEW_PROJECT_NAME, context); + cc.settingsRepo.setValue(IdeContext.DEFAULT_SETTINGS_REPO_URL); + cc.skipTools.setValue(true); + + // act - run the create command + assertThatThrownBy(cc::run) + .isInstanceOf(CliException.class) + .hasMessageContaining( + "Settings repository integrity check failed: The given git repository URL does not point to a valid settings or code-settings repository. Please verify and try again."); + + // assert + assertThat(context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS).resolve(NEW_PROJECT_NAME)).doesNotExist(); + } + @Test void testCreateWithDashPlaceholderAsCliArgument() { // arrange - see https://github.com/devonfw/IDEasy/issues/2106 @@ -234,7 +206,7 @@ void testCreateWithDashPlaceholderAsCliArgument() { assertThat(result).isEqualTo(0); assertThat(context).logAtError().hasNoMessageContaining("not found for commandlet"); assertThat(context).logAtInfo() - .hasMessageContaining("'-' was found for settings repository, the default settings repository"); + .hasMessageContaining("'-' was found for the repository, the default settings repository"); Path newProjectPath = context.getIdeRoot().resolve(NEW_PROJECT_NAME); assertThat(newProjectPath).exists(); } From 0cdc4870d3590c48098d601a646cc9e21b198dae Mon Sep 17 00:00:00 2001 From: laim2003 Date: Mon, 17 Aug 2026 14:49:39 +0200 Subject: [PATCH 13/89] #1695: corrected maven checkstyle recommendations Signed-off-by: laim2003 --- .../tools/ide/commandlet/AbstractUpdateCommandlet.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java index 094b30e7e8..135b5a1e6c 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java @@ -258,7 +258,8 @@ private void checkIntegrityAndMove(Path projectPath, String gitProjectName) { } case CODE_SETTINGS_COMBINED -> { - //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. (Formerly managed by the obsolete "--code" flag) + //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. + //(Formerly managed by the obsolete "--code" flag) targetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); moveProject(projectPath, targetDirectory); @@ -271,7 +272,8 @@ private void checkIntegrityAndMove(Path projectPath, String gitProjectName) { default -> { fileAccess.backup(projectPath); throw new CliException(getIntegrityCheckErrorMessage(String.format( - "The given git repository URL does not point to a valid settings or code-settings repository. Please verify and try again. Before trying again, please delete the folder %s", + "The given git repository URL does not point to a valid settings or code-settings repository. " + + "Please verify and try again. Before trying again, please delete the folder %s", this.context.getIdeHome()))); } } From 16098bea5b61d0ec5fb7f3e83df3f326a4cb8489 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Mon, 17 Aug 2026 14:49:39 +0200 Subject: [PATCH 14/89] #1695: corrected maven checkstyle recommendations Signed-off-by: laim2003 --- .../tools/ide/commandlet/AbstractUpdateCommandlet.java | 6 ++++-- .../devonfw/tools/ide/commandlet/CreateCommandletTest.java | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java index 094b30e7e8..135b5a1e6c 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java @@ -258,7 +258,8 @@ private void checkIntegrityAndMove(Path projectPath, String gitProjectName) { } case CODE_SETTINGS_COMBINED -> { - //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. (Formerly managed by the obsolete "--code" flag) + //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. + //(Formerly managed by the obsolete "--code" flag) targetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); moveProject(projectPath, targetDirectory); @@ -271,7 +272,8 @@ private void checkIntegrityAndMove(Path projectPath, String gitProjectName) { default -> { fileAccess.backup(projectPath); throw new CliException(getIntegrityCheckErrorMessage(String.format( - "The given git repository URL does not point to a valid settings or code-settings repository. Please verify and try again. Before trying again, please delete the folder %s", + "The given git repository URL does not point to a valid settings or code-settings repository. " + + "Please verify and try again. Before trying again, please delete the folder %s", this.context.getIdeHome()))); } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java index 06b3b5ca4c..bc65c8bdfc 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java @@ -186,7 +186,8 @@ void testProjectWithInvalidRepositoryNotCreated() { assertThatThrownBy(cc::run) .isInstanceOf(CliException.class) .hasMessageContaining( - "Settings repository integrity check failed: The given git repository URL does not point to a valid settings or code-settings repository. Please verify and try again."); + "Settings repository integrity check failed: " + + "The given git repository URL does not point to a valid settings or code-settings repository. Please verify and try again."); // assert assertThat(context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS).resolve(NEW_PROJECT_NAME)).doesNotExist(); From 9543028bd4488d76f0b644cad01010fd39615278 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Mon, 17 Aug 2026 15:16:53 +0200 Subject: [PATCH 15/89] #1695: formatting corrections Signed-off-by: laim2003 --- .../tools/ide/commandlet/AbstractUpdateCommandlet.java | 7 +++++-- .../devonfw/tools/ide/git/repository/RepositoryUtil.java | 7 ++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java index 135b5a1e6c..fb0bcb26f6 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java @@ -185,8 +185,11 @@ private void updateSettingsInStep(boolean codeRepository) { if (!this.context.getFileAccess().isEmptyDir(settingsPath)) { // settings folder seems to be invalid this.context.askToContinue( - "Your settings repository seems to be broken ('.git' folder not present). " + "We can fix this by moving your settings the backed up. " - + "You will be asked for the settings git URL and your settings will be cloned from scratch. " + "Do you want to proceed?"); + "Your settings repository seems to be broken ('.git' folder not present). " + + "We can fix this by moving your settings the backed up. " + + "You will be asked for the settings git URL and your settings will be cloned from scratch. " + + "Do you want to proceed?" + ); } this.context.getFileAccess().backup(settingsPath); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 9a6be63c13..7a3c5c0099 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -26,11 +26,8 @@ public static RepositoryType getRepositoryType(Path repositoryPath, String gitPr || Files.exists(repositoryPath.resolve(EnvironmentVariables.LEGACY_PROPERTIES))) { return RepositoryType.SETTINGS; } else if (gitProjectName != null - && Files.exists( - repositoryPath.resolve(IdeContext.FOLDER_SETTINGS)) - && getRepositoryType( - repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), - gitProjectName) == RepositoryType.SETTINGS) { + && Files.exists(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS)) + && getRepositoryType(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), gitProjectName) == RepositoryType.SETTINGS) { return RepositoryType.CODE_SETTINGS_COMBINED; } else if (!Files.exists(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS))) { return RepositoryType.CODE; From 7d117697c3e749202371dc124bef0393dc45b6a5 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 10:54:01 +0200 Subject: [PATCH 16/89] #1695: migrated settings update logic into its own class. Signed-off-by: laim2003 --- .../commandlet/update/SettingsUpdater.java | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java new file mode 100644 index 0000000000..2f78ef7460 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java @@ -0,0 +1,206 @@ +package com.devonfw.tools.ide.commandlet.update; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.devonfw.tools.ide.cli.CliRethrowException; +import com.devonfw.tools.ide.context.AbstractIdeContext; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.git.GitContext; +import com.devonfw.tools.ide.git.GitUrl; +import com.devonfw.tools.ide.git.repository.RepositoryType; +import com.devonfw.tools.ide.git.repository.RepositoryUtil; +import com.devonfw.tools.ide.io.FileAccess; +import com.devonfw.tools.ide.property.StringProperty; + +/** + * Handles updating/cloning of the settings repository. + * Returns a result indicating the outcome of the settings update operation. + */ +public class SettingsUpdater { + + private static final Logger LOG = LoggerFactory.getLogger(SettingsUpdater.class); + + private final AbstractIdeContext context; + private final StringProperty settingsRepoProperty; + + /** + * Result of the settings update operation. + */ + public enum ResultStatus { + /** Settings repository was updated via pull and is valid. */ + SETTINGS_UPDATED, + /** Settings repository was cloned from scratch (blank state). */ + SETTINGS_CLONED, + /** Settings update failed (could not clone or invalid repository). */ + SETTINGS_UPDATE_FAILED + } + + /** + * Result object containing the outcome and repository type. + */ + public record SettingsUpdateResult(ResultStatus status, RepositoryType repositoryType) { + } + + /** + * Creates a new SettingsUpdater. + * + * @param context the IDE context + * @param settingsRepoProperty the settings repository property from the update commandlet + */ + public SettingsUpdater(AbstractIdeContext context, StringProperty settingsRepoProperty) { + this.context = context; + this.settingsRepoProperty = settingsRepoProperty; + } + + /** + * Updates the settings repository by either pulling (if exists) or cloning (if new). + * + * @param codeRepository whether this is a code repository (skip pull if true and not forced) + * @return the result of the settings update operation + */ + public SettingsUpdateResult updateSettings(boolean codeRepository) { + + Path settingsPath = this.context.getSettingsPath(); + boolean isSettingsRepo = this.context.getGitContext().isGitRepo(settingsPath); + + // If it's a code repository and not forced, skip the pull + if (codeRepository && isSettingsRepo && !this.context.isForceMode()) { + LOG.info("Skipping git pull in settings due to code repository. Use --force-pull to enforce pulling."); + return new SettingsUpdateResult(ResultStatus.SETTINGS_UPDATED, RepositoryType.SETTINGS); + } + + if (isSettingsRepo) { + // Existing settings repository - pull updates + return pullExistingSettings(settingsPath); + } else { + // No existing settings - clone from scratch + return cloneSettings(); + } + } + + private SettingsUpdateResult pullExistingSettings(Path settingsPath) { + + GitContext gitContext = this.context.getGitContext(); + if (gitContext.hasUntrackedFiles(settingsPath)) { + gitContext.pullSafelyWithStash(settingsPath); + } else { + gitContext.pull(settingsPath); + } + this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); + return new SettingsUpdateResult(ResultStatus.SETTINGS_UPDATED, RepositoryType.SETTINGS); + } + + private SettingsUpdateResult cloneSettings() { + + try { + // Get settings URL + GitUrl gitUrl = getOrAskSettingsUrl(); + + // Use unique temp directory to avoid leftovers from previous attempts + Path tempProjectPath = createUniqueTempProjectPath(); + this.context.getGitContext().pullOrClone(gitUrl, tempProjectPath); + return checkIntegrityAndMove(tempProjectPath, gitUrl.getProjectName()); + } catch (Exception e) { + throw new CliRethrowException("Settings repository integrity check failed: " + e.getMessage(), e); + } + } + + private GitUrl getOrAskSettingsUrl() { + + String repository = this.settingsRepoProperty.getValue(); + repository = handleDefaultRepository(repository); + String userPrompt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; + String defaultUrl = IdeContext.DEFAULT_SETTINGS_REPO_URL; + LOG.info(AbstractUpdateCommandlet.MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath()); + + GitUrl gitUrl = null; + if (repository != null) { + gitUrl = GitUrl.of(repository); + } + while ((gitUrl == null) || !gitUrl.isValid()) { + repository = this.context.askForInput(userPrompt, defaultUrl); + repository = handleDefaultRepository(repository); + gitUrl = GitUrl.of(repository); + if (!gitUrl.isValid()) { + LOG.warn("The input URL is not valid, please try again."); + } + } + return gitUrl; + } + + private Path createUniqueTempProjectPath() { + + // Use FileAccess.createTempDir to ensure unique directory and avoid leftovers + FileAccess fileAccess = this.context.getFileAccess(); + Path tempProjectsDir = this.context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS); + fileAccess.mkdirs(tempProjectsDir); + return fileAccess.createTempDir(this.context.getProjectName() + "-"); + } + + private SettingsUpdateResult checkIntegrityAndMove(Path projectPath, String gitProjectName) { + + FileAccess fileAccess = this.context.getFileAccess(); + + if (!Files.exists(projectPath)) { + throw new CliRethrowException("Git pull target folder does not exist."); + } + + Path targetDirectory; + RepositoryType repoType = RepositoryUtil.getRepositoryType(projectPath, gitProjectName); + + switch (repoType) { + case SETTINGS -> { + targetDirectory = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); + moveProject(projectPath, targetDirectory); + this.context.getGitContext().saveCurrentCommitId(targetDirectory, this.context.getSettingsCommitIdPath()); + return new SettingsUpdateResult(ResultStatus.SETTINGS_CLONED, RepositoryType.SETTINGS); + } + case CODE_SETTINGS_COMBINED -> { + // Special case: symlink from IDE_HOME/settings to workspace/repo_name/settings + targetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); + moveProject(projectPath, targetDirectory); + + Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); + Path symlinkTargetPath = this.context.getWorkspacePath().resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS); + + fileAccess.symlink(symlinkTargetPath, symlinkPath); + this.context.getGitContext().saveCurrentCommitId(symlinkTargetPath, this.context.getSettingsCommitIdPath()); + return new SettingsUpdateResult(ResultStatus.SETTINGS_CLONED, RepositoryType.CODE_SETTINGS_COMBINED); + } + default -> { + fileAccess.backup(projectPath); + throw new CliRethrowException(getIntegrityCheckErrorMessage(String.format( + "The given git repository URL does not point to a valid settings or code-settings repository. " + + "Please verify and try again. Before trying again, please delete the folder %s", + this.context.getIdeHome()))); + } + } + } + + private Path moveProject(Path from, Path to) { + + FileAccess fileAccess = this.context.getFileAccess(); + try { + fileAccess.move(from, to); + } catch (Exception e) { + throw new CliRethrowException(String.format("Failed to move project from %s to %s", from, to), e); + } + return to; + } + + private String getIntegrityCheckErrorMessage(String message) { + return String.format("Settings repository integrity check failed: %s", message); + } + + private String handleDefaultRepository(String repository) { + if ("-".equals(repository)) { + 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; + } +} From e44afae979f5b2b604eebb4f8674ddfbd6bad10e Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 10:58:26 +0200 Subject: [PATCH 17/89] #1695: migrated settings update logic into SettingsUpdater class, added recursivity check to RepositoryUtil Signed-off-by: laim2003 --- .../tools/ide/cli/CliRethrowException.java | 31 ++++ .../ide/commandlet/CommandletManagerImpl.java | 1 + .../ide/commandlet/CreateCommandlet.java | 1 + .../AbstractUpdateCommandlet.java | 148 +++--------------- .../commandlet/update/SettingsUpdater.java | 8 +- .../{ => update}/UpdateCommandlet.java | 3 +- .../tools/ide/context/AbstractIdeContext.java | 2 +- .../devonfw/tools/ide/context/IdeContext.java | 3 +- .../ide/git/repository/RepositoryType.java | 19 +++ .../ide/git/repository/RepositoryUtil.java | 41 +++-- .../ide/commandlet/UpdateCommandletTest.java | 1 + 11 files changed, 108 insertions(+), 150 deletions(-) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java rename cli/src/main/java/com/devonfw/tools/ide/commandlet/{ => update}/AbstractUpdateCommandlet.java (67%) rename cli/src/main/java/com/devonfw/tools/ide/commandlet/{ => update}/UpdateCommandlet.java (85%) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java new file mode 100644 index 0000000000..4d3ad013a6 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java @@ -0,0 +1,31 @@ +package com.devonfw.tools.ide.cli; + + +/** + * {@link CliException} that is thrown to immediately abort the CLI process when a critical guardrail fails + * (e.g., settings repository cannot be cloned or validated). This ensures the process stops rather than + * continuing in an invalid state. + */ +public final class CliRethrowException extends CliException { + + /** + * The constructor. + * + * @param message the {@link #getMessage() message}. + */ + public CliRethrowException(String message) { + + super(message); + } + + /** + * The constructor. + * + * @param message the {@link #getMessage() message}. + * @param cause the {@link #getCause() cause}. + */ + public CliRethrowException(String message, Throwable cause) { + + super(message, cause); + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java index 0d45e767ca..dbbb47b139 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java @@ -14,6 +14,7 @@ import com.devonfw.tools.ide.cli.CliArgument; import com.devonfw.tools.ide.cli.CliArguments; import com.devonfw.tools.ide.commandlet.cleanup.CleanupCommandlet; +import com.devonfw.tools.ide.commandlet.update.UpdateCommandlet; import com.devonfw.tools.ide.completion.CompletionCandidateCollector; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.git.repository.RepositoryCommandlet; diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index ed2e937404..c04dbb71a0 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -7,6 +7,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.devonfw.tools.ide.commandlet.update.AbstractUpdateCommandlet; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.log.IdeLogLevel; diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java similarity index 67% rename from cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java rename to cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 403d8ff472..269bef49a3 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -1,4 +1,4 @@ -package com.devonfw.tools.ide.commandlet; +package com.devonfw.tools.ide.commandlet.update; import java.io.IOException; import java.nio.file.Files; @@ -12,14 +12,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.devonfw.tools.ide.cli.CliException; +import com.devonfw.tools.ide.cli.CliRethrowException; +import com.devonfw.tools.ide.commandlet.Commandlet; +import com.devonfw.tools.ide.commandlet.CommandletManager; +import com.devonfw.tools.ide.commandlet.CreateCommandlet; import com.devonfw.tools.ide.context.AbstractIdeContext; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.context.IdeStartContextImpl; -import com.devonfw.tools.ide.git.GitContext; -import com.devonfw.tools.ide.git.GitUrl; import com.devonfw.tools.ide.git.repository.RepositoryCommandlet; -import com.devonfw.tools.ide.git.repository.RepositoryUtil; import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.property.FlagProperty; import com.devonfw.tools.ide.property.StringProperty; @@ -44,17 +44,6 @@ public abstract class AbstractUpdateCommandlet extends Commandlet { private static final Logger LOG = LoggerFactory.getLogger(AbstractUpdateCommandlet.class); - private static final String MESSAGE_CODE_REPO_URL = """ - No code repository was given after '--code'. - Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc - Please enter the code repository below that includes your settings folder."""; - - private static final String MESSAGE_SETTINGS_REPO_URL = """ - No settings found at {} and no SETTINGS_URL is defined. - Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc - Please contact the technical lead of your project to get the SETTINGS_URL for your project to enter. - In case you just want to test IDEasy you may simply hit return to install the default settings."""; - /** {@link StringProperty} for the settings repository URL. */ public final StringProperty settingsRepo; @@ -168,7 +157,7 @@ protected void updateSettings() { LOG.info("Skipping git pull in settings due to code repository. Use --force-pull to enforce pulling."); return; } - this.context.newStep(getStepMessage()).run(() -> updateSettingsInStep(codeRepository), true); + this.context.newStep(getStepMessage()).run(this::updateSettingsInStep, true); } protected String getStepMessage() { @@ -176,127 +165,26 @@ protected String getStepMessage() { return "update (pull) settings repository"; } - private void updateSettingsInStep(boolean codeRepository) { - Path settingsPath = this.context.getSettingsPath(); - if (!codeRepository) { - boolean settingsRepository = this.context.getGitContext().isGitRepo(settingsPath); - if (!settingsRepository) { - if (Files.exists(settingsPath)) { - if (!this.context.getFileAccess().isEmptyDir(settingsPath)) { - // settings folder seems to be invalid - this.context.askToContinue( - "Your settings repository seems to be broken ('.git' folder not present). " - + "We can fix this by moving your settings the backed up. " - + "You will be asked for the settings git URL and your settings will be cloned from scratch. " - + "Do you want to proceed?" - ); - } - this.context.getFileAccess().backup(settingsPath); - } - //settings folder does not exist (yet), lets retrieve the settings url to pull - GitUrl gitUrl = getOrAskSettingsUrl(); - pullAndCheckIntegrity(gitUrl); - return; - } - } - GitContext gitContext = this.context.getGitContext(); - if (gitContext.hasUntrackedFiles(settingsPath)) { - gitContext.pullSafelyWithStash(settingsPath); - } else { - gitContext.pull(settingsPath); - } - this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); - } - - private GitUrl getOrAskSettingsUrl() { - - String repository = this.settingsRepo.getValue(); - repository = handleDefaultRepository(repository); - String userPrompt = "Settings 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); - } - while ((gitUrl == null) || !gitUrl.isValid()) { - repository = this.context.askForInput(userPrompt, defaultUrl); - repository = handleDefaultRepository(repository); - gitUrl = GitUrl.of(repository); - if (!gitUrl.isValid()) { - LOG.warn("The input URL is not valid, please try again."); - } - } - return gitUrl; - } - - /** - * We pull the settings repo from the remote into a temporary folder to perform health checks. - */ - private void pullAndCheckIntegrity(GitUrl gitUrl) { - GitContext gitContext = this.context.getGitContext(); - Path tempProjectPath = this.context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS).resolve(this.context.getProjectName()); - - gitContext.pullOrClone(gitUrl, tempProjectPath); - - checkIntegrityAndMove(tempProjectPath, gitUrl.getProjectName()); - } - - private void checkIntegrityAndMove(Path projectPath, String gitProjectName) { - - FileAccess fileAccess = this.context.getFileAccess(); - - if (!Files.exists(projectPath)) { - throw new CliException(getIntegrityCheckErrorMessage("Git pull target folder does not exist.")); - } + private void updateSettingsInStep() { + boolean codeRepository = this.context.isSettingsCodeRepository(); - Path targetDirectory; - switch (RepositoryUtil.getRepositoryType(projectPath, gitProjectName)) { - case SETTINGS -> { + SettingsUpdater settingsUpdater = new SettingsUpdater((AbstractIdeContext) this.context, this.settingsRepo); + SettingsUpdater.SettingsUpdateResult result = settingsUpdater.updateSettings(codeRepository); - targetDirectory = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); - moveProject(projectPath, targetDirectory); - this.context.getGitContext().saveCurrentCommitId(targetDirectory, this.context.getSettingsCommitIdPath()); + // Handle the result + switch (result.status()) { + case SETTINGS_UPDATED -> { + LOG.info("Settings repository updated successfully (type: {}).", result.repositoryType()); } - case CODE_SETTINGS_COMBINED -> { - - //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. - //(Formerly managed by the obsolete "--code" flag) - targetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); - moveProject(projectPath, targetDirectory); - - Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); - Path symlinkTargetPath = this.context.getWorkspacePath().resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS); - - fileAccess.symlink(symlinkTargetPath, symlinkPath); - this.context.getGitContext().saveCurrentCommitId(symlinkTargetPath, this.context.getSettingsCommitIdPath()); + case SETTINGS_CLONED -> { + LOG.info("Settings repository cloned successfully (type: {}).", result.repositoryType()); } - default -> { - fileAccess.backup(projectPath); - throw new CliException(getIntegrityCheckErrorMessage(String.format( - "The given git repository URL does not point to a valid settings or code-settings repository. " - + "Please verify and try again. Before trying again, please delete the folder %s", - this.context.getIdeHome()))); + case SETTINGS_UPDATE_FAILED -> { + throw new CliRethrowException("Settings repository update failed (type: " + result.repositoryType() + ")"); } } } - private Path moveProject(Path from, Path to) { - - FileAccess fileAccess = this.context.getFileAccess(); - try { - fileAccess.move(from, to); - } catch (Exception e) { - throw new CliException(getIntegrityCheckErrorMessage(String.format("Failed to move project from %s to %s", from, to)), e); - } - return to; - } - - private String getIntegrityCheckErrorMessage(String message) { - return String.format("Settings repository integrity check failed: %s", message); - } - private String handleDefaultRepository(String repository) { if ("-".equals(repository)) { LOG.info("'-' was found for the repository, the default settings repository '{}' will be used.", IdeContext.DEFAULT_SETTINGS_REPO_URL); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java index 2f78ef7460..4bf35a7ff3 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java @@ -27,6 +27,12 @@ public class SettingsUpdater { private final AbstractIdeContext context; private final StringProperty settingsRepoProperty; + private static final String MESSAGE_SETTINGS_REPO_URL = """ + No settings found at {} and no SETTINGS_URL is defined. + Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc + Please contact the technical lead of your project to get the SETTINGS_URL for your project to enter. + In case you just want to test IDEasy you may simply hit return to install the default settings."""; + /** * Result of the settings update operation. */ @@ -115,7 +121,7 @@ private GitUrl getOrAskSettingsUrl() { repository = handleDefaultRepository(repository); String userPrompt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; String defaultUrl = IdeContext.DEFAULT_SETTINGS_REPO_URL; - LOG.info(AbstractUpdateCommandlet.MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath()); + LOG.info(MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath()); GitUrl gitUrl = null; if (repository != null) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/UpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/UpdateCommandlet.java similarity index 85% rename from cli/src/main/java/com/devonfw/tools/ide/commandlet/UpdateCommandlet.java rename to cli/src/main/java/com/devonfw/tools/ide/commandlet/update/UpdateCommandlet.java index 944a4c0eeb..a0319dbb3b 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/UpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/UpdateCommandlet.java @@ -1,5 +1,6 @@ -package com.devonfw.tools.ide.commandlet; +package com.devonfw.tools.ide.commandlet.update; +import com.devonfw.tools.ide.commandlet.Commandlet; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.migration.IdeMigrator; diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java index 8dfcabaa17..0b430e4dcb 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java @@ -35,8 +35,8 @@ import com.devonfw.tools.ide.commandlet.CommandletManagerImpl; import com.devonfw.tools.ide.commandlet.ContextCommandlet; import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet; -import com.devonfw.tools.ide.commandlet.UpdateCommandlet; import com.devonfw.tools.ide.commandlet.UpgradeCommandlet; +import com.devonfw.tools.ide.commandlet.update.UpdateCommandlet; import com.devonfw.tools.ide.common.SystemPath; import com.devonfw.tools.ide.completion.CompletionCandidate; import com.devonfw.tools.ide.completion.CompletionCandidateCollector; diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java index 7516fae64d..013591e2a8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java @@ -11,6 +11,7 @@ import com.devonfw.tools.ide.cli.CliException; import com.devonfw.tools.ide.cli.CliOfflineException; import com.devonfw.tools.ide.commandlet.CommandletManager; +import com.devonfw.tools.ide.commandlet.update.AbstractUpdateCommandlet; import com.devonfw.tools.ide.common.SystemPath; import com.devonfw.tools.ide.environment.EnvironmentVariables; import com.devonfw.tools.ide.environment.EnvironmentVariablesType; @@ -69,7 +70,7 @@ public interface IdeContext extends IdeStartContext { /** * The default settings URL. * - * @see com.devonfw.tools.ide.commandlet.AbstractUpdateCommandlet + * @see AbstractUpdateCommandlet */ String DEFAULT_SETTINGS_REPO_URL = "https://github.com/devonfw/ide-settings.git"; diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java new file mode 100644 index 0000000000..81195bc178 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java @@ -0,0 +1,19 @@ +package com.devonfw.tools.ide.git.repository; + +/** + * Enum representation of a detected {@link RepositoryType}. + */ +public enum RepositoryType { + + /** Git Repository is a code repository. */ + CODE, + + /** Git Repository is a settings repository. */ + SETTINGS, + + /** A combined code & settings repository contains both the settings-folder and the code within the workspace folder. */ + CODE_SETTINGS_COMBINED, + + /** The type of the repository could not be determined. */ + UNKNOWN +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 7a3c5c0099..23c6a3663c 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -6,18 +6,39 @@ import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.environment.EnvironmentVariables; -/// Utility class for IDEasy settings/code repositories +/** + * Utility class for IDEasy settings/code repositories. + */ public class RepositoryUtil { /** - * Checks whether te given git repository is a settings repository, a combined settings and code repository, or a typical code repositor. Combined code and + * Checks whether the given git repository is a settings repository, a combined settings and code repository, or a typical code repository. Combined code and * settings repository is detected by checking whether IDE_HOME/workspaces/main/[gitProjectName]/settings exists and is a valid settings repository. * - * @param repositoryPath - The path of the repository to be checked. + * @param repositoryPath the path of the repository to be checked. + * @param gitProjectName the name of the git project. * @return {@link RepositoryType} of the repository. */ public static RepositoryType getRepositoryType(Path repositoryPath, String gitProjectName) { + return getRepositoryType(repositoryPath, gitProjectName, 0); + } + + /** + * Internal recursive method with depth tracking to prevent infinite recursion. + * + * @param repositoryPath the path of the repository to be checked. + * @param gitProjectName the name of the git project. + * @param depth the current recursion depth. + * @return {@link RepositoryType} of the repository. + */ + private static RepositoryType getRepositoryType(Path repositoryPath, String gitProjectName, int depth) { + + // Prevent infinite recursion by limiting depth (max 2 levels: root -> settings) + if (depth > 2) { + return RepositoryType.UNKNOWN; + } + if (!Files.exists(repositoryPath)) { return RepositoryType.UNKNOWN; } @@ -27,23 +48,11 @@ public static RepositoryType getRepositoryType(Path repositoryPath, String gitPr return RepositoryType.SETTINGS; } else if (gitProjectName != null && Files.exists(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS)) - && getRepositoryType(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), gitProjectName) == RepositoryType.SETTINGS) { + && getRepositoryType(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), gitProjectName, depth + 1) == RepositoryType.SETTINGS) { return RepositoryType.CODE_SETTINGS_COMBINED; } else if (!Files.exists(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS))) { return RepositoryType.CODE; } return RepositoryType.UNKNOWN; } - - /// enum representation of a detected {@link RepositoryType} - public enum RepositoryType { - /// Git Repository is a code repository. - CODE, - /// Git Repository is a settings repository. - SETTINGS, - /// A combined code & settings repository contains both the settings-folder and the code within the workspace folder. - CODE_SETTINGS_COMBINED, - /// The type of the repository could not be determined. - UNKNOWN - } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java index 626d0b0aa4..cb23034a64 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import com.devonfw.tools.ide.commandlet.update.UpdateCommandlet; import com.devonfw.tools.ide.context.AbstractIdeContextTest; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.context.IdeTestContext; From 1aa42717035b1624cedfb640a5915ba865ca8912 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 13:14:34 +0200 Subject: [PATCH 18/89] #1695: only create project structure when health check succeeded Signed-off-by: laim2003 --- .../tools/ide/commandlet/CreateCommandlet.java | 17 ++++++++++++++--- .../update/AbstractUpdateCommandlet.java | 8 -------- .../update/SettingsUpdateResultStatus.java | 4 ++++ .../ide/commandlet/update/SettingsUpdater.java | 11 ++++++++--- 4 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdateResultStatus.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index c04dbb71a0..673a1fe598 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -57,13 +57,24 @@ protected void doRun() { 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); - } else { - this.context.getFileAccess().mkdirs(newProjectPath); } + // First run the settings update (super.doRun()) to validate the settings repository + // Only if that succeeds, we create the project structure + try { + super.doRun(); + } catch (Exception e) { + // If settings update fails, clean up any temp directories and rethrow + throw e; + } + + // Settings update succeeded, now create the project structure + if (!this.context.getFileAccess().isEmptyDir(newProjectPath)) { + this.context.getFileAccess().backup(newProjectPath); + } + this.context.getFileAccess().mkdirs(newProjectPath); initializeProject(newProjectPath); this.context.setIdeHome(newProjectPath); - super.doRun(); this.context.getFileAccess().writeFileContent(IdeVersion.getVersionString(), newProjectPath.resolve(IdeContext.FILE_SOFTWARE_VERSION)); IdeLogLevel.SUCCESS.log(LOG, "Successfully created new project '{}'.", newProjectName); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 269bef49a3..d69628e2f4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -185,14 +185,6 @@ private void updateSettingsInStep() { } } - private String handleDefaultRepository(String repository) { - if ("-".equals(repository)) { - 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 updateSoftware() { if (this.skipTools.isTrue()) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdateResultStatus.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdateResultStatus.java new file mode 100644 index 0000000000..4bdf281b7e --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdateResultStatus.java @@ -0,0 +1,4 @@ +package com.devonfw.tools.ide.commandlet.update; + +public enum SettingsUpdateResultStatus { +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java index 4bf35a7ff3..14b7b6edff 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java @@ -17,8 +17,7 @@ import com.devonfw.tools.ide.property.StringProperty; /** - * Handles updating/cloning of the settings repository. - * Returns a result indicating the outcome of the settings update operation. + * Handles updating/cloning of the settings repository. Returns a result indicating the outcome of the settings update operation. */ public class SettingsUpdater { @@ -49,6 +48,7 @@ public enum ResultStatus { * Result object containing the outcome and repository type. */ public record SettingsUpdateResult(ResultStatus status, RepositoryType repositoryType) { + } /** @@ -102,15 +102,20 @@ private SettingsUpdateResult pullExistingSettings(Path settingsPath) { private SettingsUpdateResult cloneSettings() { + Path tempProjectPath = null; try { // Get settings URL GitUrl gitUrl = getOrAskSettingsUrl(); // Use unique temp directory to avoid leftovers from previous attempts - Path tempProjectPath = createUniqueTempProjectPath(); + tempProjectPath = createUniqueTempProjectPath(); this.context.getGitContext().pullOrClone(gitUrl, tempProjectPath); return checkIntegrityAndMove(tempProjectPath, gitUrl.getProjectName()); } catch (Exception e) { + // Clean up temp directory on failure + if (tempProjectPath != null) { + this.context.getFileAccess().backup(tempProjectPath); + } throw new CliRethrowException("Settings repository integrity check failed: " + e.getMessage(), e); } } From 80a4715e476e1902c234cc3d818c4078bec4de5b Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:18:07 +0200 Subject: [PATCH 19/89] #1695: extended CliException with isForceRethrowInStep() flag --- .../com/devonfw/tools/ide/cli/CliException.java | 11 +++++++++++ .../devonfw/tools/ide/cli/CliRethrowException.java | 13 +++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliException.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliException.java index 16cb0598fe..3a39a319a4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/cli/CliException.java +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliException.java @@ -64,4 +64,15 @@ public int getExitCode() { return this.exitCode; } + /** + * @return {@code true} if this exception has to be re-thrown from a {@link com.devonfw.tools.ide.step.Step Step} even if that {@code Step} was not asked to + * re-throw errors, {@code false} otherwise (default). A regular error only makes the according {@code Step} fail while the overall process continues with + * the next step. However, if a critical guardrail was violated (e.g. no valid settings could be established) continuing makes no sense and the entire + * process has to be aborted. + */ + public boolean isForceRethrowInStep() { + + return false; + } + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java index 4d3ad013a6..fc991233bd 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java @@ -1,10 +1,9 @@ package com.devonfw.tools.ide.cli; - /** - * {@link CliException} that is thrown to immediately abort the CLI process when a critical guardrail fails - * (e.g., settings repository cannot be cloned or validated). This ensures the process stops rather than - * continuing in an invalid state. + * {@link CliException} that aborts the entire CLI process when a critical guardrail fails (e.g. the settings repository could not be cloned or is not a valid + * settings repository). Unlike a regular error that only makes the current {@link com.devonfw.tools.ide.step.Step Step} fail while the overall process + * continues, this exception {@link #isForceRethrowInStep() is always re-thrown} so no further step is executed in an invalid state. */ public final class CliRethrowException extends CliException { @@ -28,4 +27,10 @@ public CliRethrowException(String message, Throwable cause) { super(message, cause); } + + @Override + public boolean isForceRethrowInStep() { + + return true; + } } From 85a584a31e8c231ab13a90ca85801472080f7708 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:21:55 +0200 Subject: [PATCH 20/89] #1695: CreateCommandlet now only creates the project structure after the health checks succeeded. --- .../ide/commandlet/CreateCommandlet.java | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index 673a1fe598..bc95410bd5 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -51,42 +51,43 @@ public boolean isIdeHomeRequired() { @Override protected void doRun() { - String newProjectName = this.newProject.getValue(); - Path newProjectPath = this.context.getIdeRoot().resolve(newProjectName); - + Path newProjectPath = getNewProjectPath(); LOG.info("Creating new IDEasy project in {}", newProjectPath); - if (!this.context.getFileAccess().isEmptyDir(newProjectPath)) { + FileAccess fileAccess = this.context.getFileAccess(); + if (!fileAccess.isEmptyDir(newProjectPath)) { this.context.askToContinue("Directory {} already exists. Do you want to continue?", newProjectPath); + fileAccess.backup(newProjectPath); } + // point IDE_HOME to the new project before the settings are checked - this only computes the paths and creates nothing on disk so that a failing + // health check leaves no project behind. As IDE_HOME/settings does not exist yet the settings will be cloned instead of pulled. + this.context.setIdeHome(newProjectPath); + super.doRun(); + } - // First run the settings update (super.doRun()) to validate the settings repository - // Only if that succeeds, we create the project structure - try { - super.doRun(); - } catch (Exception e) { - // If settings update fails, clean up any temp directories and rethrow - throw e; - } + @Override + protected void prepareProject() { - // Settings update succeeded, now create the project structure - if (!this.context.getFileAccess().isEmptyDir(newProjectPath)) { - this.context.getFileAccess().backup(newProjectPath); - } - this.context.getFileAccess().mkdirs(newProjectPath); - initializeProject(newProjectPath); - this.context.setIdeHome(newProjectPath); - this.context.getFileAccess().writeFileContent(IdeVersion.getVersionString(), newProjectPath.resolve(IdeContext.FILE_SOFTWARE_VERSION)); - IdeLogLevel.SUCCESS.log(LOG, "Successfully created new project '{}'.", newProjectName); + // only called after the settings passed the health check + Path newProjectPath = getNewProjectPath(); + FileAccess fileAccess = this.context.getFileAccess(); + fileAccess.mkdirs(newProjectPath); + fileAccess.mkdirs(newProjectPath.resolve(IdeContext.FOLDER_SOFTWARE)); + fileAccess.mkdirs(newProjectPath.resolve(IdeContext.FOLDER_PLUGINS)); + fileAccess.mkdirs(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)); + } + @Override + protected void finalizeProject() { + + Path newProjectPath = getNewProjectPath(); + this.context.getFileAccess().writeFileContent(IdeVersion.getVersionString(), newProjectPath.resolve(IdeContext.FILE_SOFTWARE_VERSION)); + IdeLogLevel.SUCCESS.log(LOG, "Successfully created new project '{}'.", this.newProject.getValue()); logWelcomeMessage(); } - private void initializeProject(Path newInstancePath) { + private Path getNewProjectPath() { - FileAccess fileAccess = this.context.getFileAccess(); - fileAccess.mkdirs(newInstancePath.resolve(IdeContext.FOLDER_SOFTWARE)); - fileAccess.mkdirs(newInstancePath.resolve(IdeContext.FOLDER_PLUGINS)); - fileAccess.mkdirs(newInstancePath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)); + return this.context.getIdeRoot().resolve(this.newProject.getValue()); } @Override From 81d3d0060efb1c83486e09efd2009e41aa181e17 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:54:53 +0200 Subject: [PATCH 21/89] #1695: updated RepositoryUtil --- .../ide/git/repository/RepositoryUtil.java | 55 ++++++++----------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 23c6a3663c..2eebe1d75d 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -12,47 +12,38 @@ public class RepositoryUtil { /** - * Checks whether the given git repository is a settings repository, a combined settings and code repository, or a typical code repository. Combined code and - * settings repository is detected by checking whether IDE_HOME/workspaces/main/[gitProjectName]/settings exists and is a valid settings repository. + * Checks whether the given git repository is a settings repository, a combined settings and code repository, or a typical code repository. A combined code + * and settings repository is detected by a top-level {@code settings} folder that itself is a valid settings folder. * - * @param repositoryPath the path of the repository to be checked. - * @param gitProjectName the name of the git project. - * @return {@link RepositoryType} of the repository. + * @param repositoryPath the {@link Path} to the repository to check. + * @return the {@link RepositoryType} of the repository. */ - public static RepositoryType getRepositoryType(Path repositoryPath, String gitProjectName) { + public static RepositoryType getRepositoryType(Path repositoryPath) { - return getRepositoryType(repositoryPath, gitProjectName, 0); - } - - /** - * Internal recursive method with depth tracking to prevent infinite recursion. - * - * @param repositoryPath the path of the repository to be checked. - * @param gitProjectName the name of the git project. - * @param depth the current recursion depth. - * @return {@link RepositoryType} of the repository. - */ - private static RepositoryType getRepositoryType(Path repositoryPath, String gitProjectName, int depth) { - - // Prevent infinite recursion by limiting depth (max 2 levels: root -> settings) - if (depth > 2) { - return RepositoryType.UNKNOWN; - } - - if (!Files.exists(repositoryPath)) { + if (!Files.isDirectory(repositoryPath)) { return RepositoryType.UNKNOWN; } - - if (Files.exists(repositoryPath.resolve(EnvironmentVariables.DEFAULT_PROPERTIES)) - || Files.exists(repositoryPath.resolve(EnvironmentVariables.LEGACY_PROPERTIES))) { + if (isSettingsFolder(repositoryPath)) { return RepositoryType.SETTINGS; - } else if (gitProjectName != null - && Files.exists(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS)) - && getRepositoryType(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), gitProjectName, depth + 1) == RepositoryType.SETTINGS) { + } + Path settingsFolder = repositoryPath.resolve(IdeContext.FOLDER_SETTINGS); + if (isSettingsFolder(settingsFolder)) { return RepositoryType.CODE_SETTINGS_COMBINED; - } else if (!Files.exists(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS))) { + } + if (!Files.exists(settingsFolder)) { return RepositoryType.CODE; } + // there is a settings folder but it does not contain the required properties file return RepositoryType.UNKNOWN; } + + /** + * @param folder the {@link Path} to check. + * @return {@code true} if the given {@code folder} is the root of a settings repository, {@code false} otherwise. + */ + private static boolean isSettingsFolder(Path folder) { + + return Files.exists(folder.resolve(EnvironmentVariables.DEFAULT_PROPERTIES)) + || Files.exists(folder.resolve(EnvironmentVariables.LEGACY_PROPERTIES)); + } } From 5d51ce8ee7a49cbfe6034e1863b1b439c707af45 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:55:03 +0200 Subject: [PATCH 22/89] #1695: updated documentation --- documentation/settings.adoc | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/documentation/settings.adoc b/documentation/settings.adoc index 3d7a1540a1..3bf2f8e240 100644 --- a/documentation/settings.adoc +++ b/documentation/settings.adoc @@ -51,6 +51,21 @@ But we do not want to forget the following advantage: Anyhow you can still create feature branches in standalone settings repositories to manage such scenarios and follow KISS and trunk-based development so you more or less avoid such problems. However, if you are in a monolithic project with complex release branches you may consider using the "settings in code repository" approach. +== Health check + +Whenever `IDEasy` clones or updates your settings it first clones the git repository into a temporary directory and performs a health check on it: + +* the given git URL has to be valid, +* cloning the repository has to succeed, +* and the repository has to be a settings repository or a combined code and settings repository (see link:#code-repository[above]). + +Only if this health check succeeded the settings are installed: an existing settings repository is updated via `git pull` while a new one is moved from the temporary directory to its final location. +This way a broken or wrong git URL can never leave you with a damaged project. +In particular `ide create` will not create the project at all if the health check fails, so you can simply fix the URL and try again. + +If you are sure that you know better, you can use the `--force` option. +`IDEasy` will then still report the problem but ask you whether you want to continue anyway. + == Structure The settings folder has to follow this file structure: From 55d0c0a0ae727bde6c2717994b588c9983bc9655 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:56:36 +0200 Subject: [PATCH 23/89] #1695: updated CreateCommandlet --- .../tools/ide/commandlet/CreateCommandlet.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index bc95410bd5..67e2256983 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -62,6 +62,9 @@ protected void doRun() { // health check leaves no project behind. As IDE_HOME/settings does not exist yet the settings will be cloned instead of pulled. this.context.setIdeHome(newProjectPath); super.doRun(); + this.context.getFileAccess().writeFileContent(IdeVersion.getVersionString(), newProjectPath.resolve(IdeContext.FILE_SOFTWARE_VERSION)); + IdeLogLevel.SUCCESS.log(LOG, "Successfully created new project '{}'.", this.newProject.getValue()); + logWelcomeMessage(); } @Override @@ -76,15 +79,6 @@ protected void prepareProject() { fileAccess.mkdirs(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)); } - @Override - protected void finalizeProject() { - - Path newProjectPath = getNewProjectPath(); - this.context.getFileAccess().writeFileContent(IdeVersion.getVersionString(), newProjectPath.resolve(IdeContext.FILE_SOFTWARE_VERSION)); - IdeLogLevel.SUCCESS.log(LOG, "Successfully created new project '{}'.", this.newProject.getValue()); - logWelcomeMessage(); - } - private Path getNewProjectPath() { return this.context.getIdeRoot().resolve(this.newProject.getValue()); From af34c90fccecf07f527403875b5db3a2bf379aed Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:58:08 +0200 Subject: [PATCH 24/89] #1695: divided AbstractUpdateCommandlet settings update step into verify & apply steps --- .../update/AbstractUpdateCommandlet.java | 52 ++++++++++++------- .../update/SettingsUpdateResultStatus.java | 4 -- 2 files changed, 34 insertions(+), 22 deletions(-) delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdateResultStatus.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index d69628e2f4..725254c3df 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -12,10 +12,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.devonfw.tools.ide.cli.CliRethrowException; import com.devonfw.tools.ide.commandlet.Commandlet; import com.devonfw.tools.ide.commandlet.CommandletManager; import com.devonfw.tools.ide.commandlet.CreateCommandlet; +import com.devonfw.tools.ide.commandlet.update.SettingsUpdater.ResultStatus; +import com.devonfw.tools.ide.commandlet.update.SettingsUpdater.SettingsUpdateResult; import com.devonfw.tools.ide.context.AbstractIdeContext; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.context.IdeStartContextImpl; @@ -97,6 +98,15 @@ protected void doRun() { createStartScripts(); } + /** + * Hook that is called after the settings passed the health check but before they are moved to their final location. Does nothing by default and is overridden + * by {@link CreateCommandlet} to create the project structure so that no project is created at all if the health check failed. + */ + protected void prepareProject() { + + // nothing to do by default + } + private void reloadContext() { ((AbstractIdeContext) this.context).reload(); @@ -148,16 +158,18 @@ private void setupConf(Path template, Path conf) { /** * Updates the settings repository in IDE_HOME/settings by either cloning if no such repository exists or pulling if the repository exists then saves the - * latest current commit ID in the file ".commit.id". + * latest current commit ID in the file ".commit.id". The settings are always cloned into a temporary directory first where a health check is performed. Only + * if that health check succeeded the settings are pulled or the verified clone is moved to its final location. */ protected void updateSettings() { boolean codeRepository = this.context.isSettingsCodeRepository(); - if (codeRepository && !(this.context.isForceMode() || forcePull.isTrue())) { + if (codeRepository && !(this.context.isForceMode() || this.forcePull.isTrue())) { LOG.info("Skipping git pull in settings due to code repository. Use --force-pull to enforce pulling."); return; } - this.context.newStep(getStepMessage()).run(this::updateSettingsInStep, true); + Step step = this.context.newStep(getStepMessage()); + step.run(() -> updateSettingsInStep(step)); } protected String getStepMessage() { @@ -165,23 +177,27 @@ protected String getStepMessage() { return "update (pull) settings repository"; } - private void updateSettingsInStep() { - boolean codeRepository = this.context.isSettingsCodeRepository(); - - SettingsUpdater settingsUpdater = new SettingsUpdater((AbstractIdeContext) this.context, this.settingsRepo); - SettingsUpdater.SettingsUpdateResult result = settingsUpdater.updateSettings(codeRepository); + private void updateSettingsInStep(Step step) { - // Handle the result - switch (result.status()) { - case SETTINGS_UPDATED -> { - LOG.info("Settings repository updated successfully (type: {}).", result.repositoryType()); - } - case SETTINGS_CLONED -> { - LOG.info("Settings repository cloned successfully (type: {}).", result.repositoryType()); + SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo); + try { + SettingsUpdateResult result = this.context.newStep("Performing health check on settings").call(settingsUpdater::checkSettings, () -> null); + // fatal problems (e.g. no valid settings at all) were already rethrown, so reaching this point means the settings we have stay usable + if (result == null) { + step.error("Health check on settings failed - the settings have not been updated."); + return; + } else if (result.status() == ResultStatus.SETTINGS_UPDATE_FAILED) { + step.error("The settings have not been updated: {}", result.errorMessage()); + return; } - case SETTINGS_UPDATE_FAILED -> { - throw new CliRethrowException("Settings repository update failed (type: " + result.repositoryType() + ")"); + prepareProject(); + boolean applied = this.context.newStep("Applying update").run(() -> settingsUpdater.applySettings(result)); + if (!applied) { + step.error("Failed to apply the settings update."); } + } finally { + // the verified clone lives across both steps and the prepareProject hook so it is only here that its lifetime ends + settingsUpdater.cleanup(); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdateResultStatus.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdateResultStatus.java deleted file mode 100644 index 4bdf281b7e..0000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdateResultStatus.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.devonfw.tools.ide.commandlet.update; - -public enum SettingsUpdateResultStatus { -} From 0a10bc916ef3a4ba32fdfbd479b495d08381a050 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:58:57 +0200 Subject: [PATCH 25/89] #1695: updated GitContextMock to use the default settings repo URL as a mock URL (for testing new workflow) --- .../test/java/com/devonfw/tools/ide/git/GitContextMock.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java index 10a5e3f023..27723760ac 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java @@ -20,7 +20,8 @@ */ public class GitContextMock extends GitContextImpl { - private static final String MOCKED_URL_VALUE = "mocked url value"; + /** Fallback URL for repositories without a mocked {@code .git/config} - has to be a {@link GitUrl#isValid() valid} git URL. */ + private static final String MOCKED_URL_VALUE = DEFAULT_SETTINGS_GIT_URL; /** Filename used to persist mocked remotes inside the {@code .git} folder. */ private static final String REMOTES_FILE = "remotes.properties"; From bae4a08fe5139f28dc21735c19806ec3b4e393cc Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:59:34 +0200 Subject: [PATCH 26/89] #1695: updated Step implementation to support isForceRethrow --- .../java/com/devonfw/tools/ide/step/Step.java | 29 ++++++++-- .../com/devonfw/tools/ide/step/StepTest.java | 57 +++++++++++++++++++ cli/src/test/resources/code-settings/pom.xml | 1 + .../code-settings/settings/ide.properties | 1 + 4 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 cli/src/test/resources/code-settings/pom.xml create mode 100644 cli/src/test/resources/code-settings/settings/ide.properties diff --git a/cli/src/main/java/com/devonfw/tools/ide/step/Step.java b/cli/src/main/java/com/devonfw/tools/ide/step/Step.java index 19760d9723..800caa95ab 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/step/Step.java +++ b/cli/src/main/java/com/devonfw/tools/ide/step/Step.java @@ -3,6 +3,8 @@ import java.util.concurrent.Callable; import java.util.function.Supplier; +import com.devonfw.tools.ide.cli.CliException; + /** * Interface for a {@link Step} of the process. Allows to split larger processes into smaller steps that are traced and measured. Also prevents that if one step * fails, the overall process can still continue so a sub-step (e.g. "plugin installation" or "git update") does not automatically block the entire process. At @@ -219,7 +221,8 @@ default boolean run(Runnable stepCode) { /** * @param stepCode the {@link Runnable} to {@link Runnable#run() execute} for this {@link Step}. - * @param rethrow - {@code true} to rethrow a potential {@link Throwable error}. + * @param rethrow - {@code true} to rethrow a potential {@link Throwable error}. Independent of this flag an error is always rethrown if it + * {@link CliException#isForceRethrowInStep() forces} it. * @return {@code true} on success, {@code false} on error (if {@code rethrow} is {@code false}). */ default boolean run(Runnable stepCode, boolean rethrow) { @@ -231,8 +234,10 @@ default boolean run(Runnable stepCode, boolean rethrow) { } return true; } catch (RuntimeException | Error e) { - error(e); - if (rethrow) { + boolean forceRethrow = isForceRethrow(e); + // if the error is rethrown it gets logged by the caller so we suppress duplicated error messages here + error(e, forceRethrow); + if (rethrow || forceRethrow) { throw e; } return false; @@ -264,7 +269,8 @@ default R call(Callable stepCode, Supplier resultOnErrorSupplier) { /** * @param stepCode the {@link Callable} to {@link Callable#call() execute} for this {@link Step}. - * @param rethrow - {@code true} to rethrow a potential {@link Throwable error}. + * @param rethrow - {@code true} to rethrow a potential {@link Throwable error}. Independent of this flag an error is always rethrown if it + * {@link CliException#isForceRethrowInStep() forces} it. * @param resultOnErrorSupplier the {@link Supplier} {@link Supplier#get() providing} the result to be returned in case of a {@link Throwable error}. * @param type of the return value. * @return the value returned from {@link Callable#call()}. @@ -278,8 +284,10 @@ default R call(Callable stepCode, boolean rethrow, Supplier resultOnEr } return result; } catch (Throwable e) { - error(e); - if (rethrow) { + boolean forceRethrow = isForceRethrow(e); + // if the error is rethrown it gets logged by the caller so we suppress duplicated error messages here + error(e, forceRethrow); + if (rethrow || forceRethrow) { if (e instanceof RuntimeException re) { throw re; } else if (e instanceof Error error) { @@ -294,4 +302,13 @@ default R call(Callable stepCode, boolean rethrow, Supplier resultOnEr } } + /** + * @param error the {@link Throwable} that occurred inside a {@link Step}. + * @return {@code true} if the given {@code error} has to be rethrown even if the {@link Step} was not asked to rethrow errors, {@code false} otherwise. + */ + private static boolean isForceRethrow(Throwable error) { + + return (error instanceof CliException cliException) && cliException.isForceRethrowInStep(); + } + } diff --git a/cli/src/test/java/com/devonfw/tools/ide/step/StepTest.java b/cli/src/test/java/com/devonfw/tools/ide/step/StepTest.java index 0a5b87d12c..bbc4d9332f 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/step/StepTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/step/StepTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Test; +import com.devonfw.tools.ide.cli.CliRethrowException; import com.devonfw.tools.ide.context.AbstractIdeContextTest; import com.devonfw.tools.ide.context.IdeTestContext; import com.devonfw.tools.ide.log.IdeLogEntry; @@ -130,4 +131,60 @@ void testInvalidUsageErrorSuccess() { IdeLogEntry.ofDebug("Step 'Test-Step' ended successfully.")); } + @Test + void testRunSwallowsRegularError() { + + // arrange + IdeTestContext context = newContext(PROJECT_BASIC, "project", false); + Step step = context.newStep("Test-Step"); + // act + boolean success = step.run(() -> { + throw new IllegalStateException("regular error"); + }); + // assert + assertThat(success).isFalse(); + assertThat(step.isFailure()).isTrue(); + } + + @Test + void testRunRethrowsForcedError() { + + // arrange + IdeTestContext context = newContext(PROJECT_BASIC, "project", false); + Step step = context.newStep("Test-Step"); + // act & assert + assertThatThrownBy(() -> step.run(() -> { + throw new CliRethrowException("fatal error"); + })).isInstanceOf(CliRethrowException.class).hasMessage("fatal error"); + assertThat(step.isFailure()).isTrue(); + } + + @Test + void testCallReturnsFallbackOnRegularError() { + + // arrange + IdeTestContext context = newContext(PROJECT_BASIC, "project", false); + Step step = context.newStep("Test-Step"); + // act + String result = step.call(() -> { + throw new IllegalStateException("regular error"); + }, () -> "fallback"); + // assert + assertThat(result).isEqualTo("fallback"); + assertThat(step.isFailure()).isTrue(); + } + + @Test + void testCallRethrowsForcedError() { + + // arrange + IdeTestContext context = newContext(PROJECT_BASIC, "project", false); + Step step = context.newStep("Test-Step"); + // act & assert + assertThatThrownBy(() -> step.call(() -> { + throw new CliRethrowException("fatal error"); + }, () -> "fallback")).isInstanceOf(CliRethrowException.class).hasMessage("fatal error"); + assertThat(step.isFailure()).isTrue(); + } + } diff --git a/cli/src/test/resources/code-settings/pom.xml b/cli/src/test/resources/code-settings/pom.xml new file mode 100644 index 0000000000..6d465deda5 --- /dev/null +++ b/cli/src/test/resources/code-settings/pom.xml @@ -0,0 +1 @@ +code diff --git a/cli/src/test/resources/code-settings/settings/ide.properties b/cli/src/test/resources/code-settings/settings/ide.properties new file mode 100644 index 0000000000..28913aee06 --- /dev/null +++ b/cli/src/test/resources/code-settings/settings/ide.properties @@ -0,0 +1 @@ +IDE_TOOLS=java,mvn From 94725de5d8bc9bb3722d180cd5199751bf9964a2 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:59:51 +0200 Subject: [PATCH 27/89] #1695: updated tests --- .../ide/commandlet/CreateCommandletTest.java | 55 +++++++++++++++++- .../ide/commandlet/UpdateCommandletTest.java | 56 +++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java index bc65c8bdfc..56e6f403cb 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java @@ -16,6 +16,7 @@ import com.devonfw.tools.ide.environment.EnvironmentVariables; import com.devonfw.tools.ide.environment.EnvironmentVariablesType; import com.devonfw.tools.ide.git.GitContextImplMock; +import com.devonfw.tools.ide.io.WindowsSymlinkTestHelper; import com.devonfw.tools.ide.version.IdeVersion; /** @@ -64,6 +65,8 @@ void testCreateCommandletRun() { assertThat(newProjectPath.resolve(IdeContext.FOLDER_PLUGINS)).exists(); assertThat(newProjectPath.resolve(IdeContext.FOLDER_SOFTWARE)).exists(); assertThat(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)).exists(); + // the settings have to be cloned into the new project and not into the project the create command was started from + assertThat(newProjectPath.resolve(IdeContext.FOLDER_SETTINGS).resolve("ide.properties")).exists(); assertThat(context.getIdeRoot().resolve("_ide/tmp/projects").resolve(NEW_PROJECT_NAME)).doesNotExist(); } @@ -189,10 +192,60 @@ void testProjectWithInvalidRepositoryNotCreated() { "Settings repository integrity check failed: " + "The given git repository URL does not point to a valid settings or code-settings repository. Please verify and try again."); - // assert + // assert - if "ide create" fails then no project shall be created at all + assertThat(context.getIdeRoot().resolve(NEW_PROJECT_NAME)).doesNotExist(); assertThat(context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS).resolve(NEW_PROJECT_NAME)).doesNotExist(); } + @Test + void testCreateWithCodeSettingsRepository() { + + // arrange - a combined code and settings repository has the settings in a top-level "settings" folder + WindowsSymlinkTestHelper.assumeSymlinksSupported(); + GitContextImplMock gitContextImplMock = new GitContextImplMock(context, TEST_RESOURCES.resolve("code-settings")); + context.setGitContext(gitContextImplMock); + CreateCommandlet cc = context.getCommandletManager().getCommandlet(CreateCommandlet.class); + cc.newProject.setValueAsString(NEW_PROJECT_NAME, context); + cc.settingsRepo.setValue("https://github.com/devonfw/code-settings-repo.git"); + cc.skipTools.setValue(true); + + // act + cc.run(); + + // assert - the repository is placed into the workspace and IDE_HOME/settings is a symlink to its settings folder + Path newProjectPath = context.getIdeRoot().resolve(NEW_PROJECT_NAME); + Path codePath = newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN).resolve("code-settings-repo"); + assertThat(codePath.resolve("pom.xml")).exists(); + assertThat(codePath.resolve(IdeContext.FOLDER_SETTINGS).resolve("ide.properties")).exists(); + Path settingsLink = newProjectPath.resolve(IdeContext.FOLDER_SETTINGS); + assertThat(settingsLink).isSymbolicLink(); + assertThat(settingsLink.resolve("ide.properties")).exists(); + } + + @Test + void testCreateWithInvalidRepositoryContinuesInForceMode() { + + // arrange - force mode lets the user decide to continue even though the health check failed + GitContextImplMock gitContextImplMock = new GitContextImplMock(context, TEST_RESOURCES.resolve("pypi")); + context.setGitContext(gitContextImplMock); + context.getStartContext().setForceMode(true); + context.setAnswers("yes"); + CreateCommandlet cc = context.getCommandletManager().getCommandlet(CreateCommandlet.class); + cc.newProject.setValueAsString(NEW_PROJECT_NAME, context); + cc.settingsRepo.setValue(IdeContext.DEFAULT_SETTINGS_REPO_URL); + cc.skipTools.setValue(true); + cc.skipRepositories.setValue(true); + + // act + cc.run(); + + // assert + Path newProjectPath = context.getIdeRoot().resolve(NEW_PROJECT_NAME); + assertThat(newProjectPath).exists(); + assertThat(context).logAtWarning() + .hasMessageContaining("does not point to a valid settings or code-settings repository"); + } + @Test void testCreateWithDashPlaceholderAsCliArgument() { // arrange - see https://github.com/devonfw/IDEasy/issues/2106 diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java index cb23034a64..8715b74b4e 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java @@ -13,6 +13,8 @@ import com.devonfw.tools.ide.context.IdeTestContext; import com.devonfw.tools.ide.environment.EnvironmentVariables; import com.devonfw.tools.ide.environment.EnvironmentVariablesType; +import com.devonfw.tools.ide.git.GitContext; +import com.devonfw.tools.ide.git.GitContextMock; import com.devonfw.tools.ide.tool.java.Java; import com.devonfw.tools.ide.tool.mvn.Mvn; import com.devonfw.tools.ide.variable.IdeVariables; @@ -156,4 +158,58 @@ void testRunUpdateSoftwareDoesNotFailWhenSettingsPathIsDeleted(WireMockRuntimeIn assertThat(context).logAtSuccess().hasMessage(SUCCESS_UPDATE_SETTINGS); assertThat(context).logAtSuccess().hasMessageContaining(SUCCESS_INSTALL_OR_UPDATE_SOFTWARE); } + + /** + * Tests that a settings folder that exists but is not a git repository is backed up and cloned from scratch after the user confirmed. + */ + @Test + void testRunUpdateWithBrokenSettingsFolder() { + + // arrange + IdeTestContext context = newContext(PROJECT_UPDATE); + Path settingsPath = context.getSettingsPath(); + // remove the '.git' folder so the settings are present but broken + context.getFileAccess().delete(settingsPath.resolve(GitContext.GIT_FOLDER)); + UpdateCommandlet update = context.getCommandletManager().getCommandlet(UpdateCommandlet.class); + // first answer confirms the backup of the broken settings, second answer picks the default settings repository + context.setAnswers("yes", "-"); + + // act + update.run(); + + // assert + assertThat(context).logAtSuccess().hasMessage(SUCCESS_UPDATE_SETTINGS); + assertThat(context).logAtInfo().hasMessageContaining("Creating backup by moving " + settingsPath); + assertThat(context.getIdeHome().resolve(IdeContext.FOLDER_BACKUPS)).exists(); + assertThat(settingsPath.resolve(GitContext.GIT_FOLDER)).exists(); + assertThat(context).logAtSuccess().hasMessageContaining(SUCCESS_INSTALL_OR_UPDATE_SOFTWARE); + } + + /** + * Tests that a failing "git pull" (e.g. due to an error of a custom git server) only fails the settings step while the software is still installed. + *

+ * See: #2335 for reference. + */ + @Test + void testRunUpdateContinuesWhenPullFails() { + + // arrange + IdeTestContext context = newContext(PROJECT_UPDATE); + context.setGitContext(new GitContextMock(context) { + @Override + public void pull(Path repository) { + + throw new IllegalStateException("git pull failed due to an error of the custom git server"); + } + }); + UpdateCommandlet update = context.getCommandletManager().getCommandlet(UpdateCommandlet.class); + + // act + update.run(); + + // assert + assertThat(context).logAtError().hasMessage("Step 'Applying update' ended with failure."); + assertThat(context).log().hasNoMessage(SUCCESS_UPDATE_SETTINGS); + assertThat(context).logAtSuccess().hasMessageContaining(SUCCESS_INSTALL_OR_UPDATE_SOFTWARE); + } } From d37044758d08c0d02a2ed26ba735fe7cf2a1ff35 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 23:00:31 +0200 Subject: [PATCH 28/89] #1695: updated SettingsUpdater to use to stage system of verify&apply (WIP) --- .../commandlet/update/SettingsUpdater.java | 350 ++++++++++++------ 1 file changed, 238 insertions(+), 112 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java index 14b7b6edff..c64c7de9e4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java @@ -6,8 +6,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.devonfw.tools.ide.cli.CliAbortException; +import com.devonfw.tools.ide.cli.CliException; import com.devonfw.tools.ide.cli.CliRethrowException; -import com.devonfw.tools.ide.context.AbstractIdeContext; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.git.GitContext; import com.devonfw.tools.ide.git.GitUrl; @@ -17,201 +18,326 @@ import com.devonfw.tools.ide.property.StringProperty; /** - * Handles updating/cloning of the settings repository. Returns a result indicating the outcome of the settings update operation. + * Handles the settings repository of the current project in two phases: + *

    + *
  1. {@link #checkSettings() health check}: the settings are always cloned into a temporary directory first where it is verified that the git URL is valid, + * that cloning succeeded, and that the repository actually is a settings or a combined code and settings repository.
  2. + *
  3. {@link #applySettings(SettingsUpdateResult) apply}: only after the health check succeeded the settings are either pulled in place (if they were already + * present) or the verified clone is moved to its final location.
  4. + *
*/ public class SettingsUpdater { private static final Logger LOG = LoggerFactory.getLogger(SettingsUpdater.class); - private final AbstractIdeContext context; - private final StringProperty settingsRepoProperty; - private static final String MESSAGE_SETTINGS_REPO_URL = """ No settings found at {} and no SETTINGS_URL is defined. Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc Please contact the technical lead of your project to get the SETTINGS_URL for your project to enter. In case you just want to test IDEasy you may simply hit return to install the default settings."""; + private static final String MESSAGE_INVALID_REPOSITORY = "Settings repository integrity check failed: " + + "The given git repository URL does not point to a valid settings or code-settings repository. Please verify and try again."; + + private final IdeContext context; + + private final StringProperty settingsRepoProperty; + + /** The temporary directory holding the verified clone or {@code null} if there is nothing to move. */ + private Path tempDir; + + /** The name of the git project - required to place a combined code and settings repository into the workspace. */ + private String gitProjectName; + /** - * Result of the settings update operation. + * Status of the settings {@link SettingsUpdater#checkSettings() health check} describing what {@link SettingsUpdater#applySettings(SettingsUpdateResult)} has + * to do. */ public enum ResultStatus { - /** Settings repository was updated via pull and is valid. */ + /** The settings repository was already present and is valid - it only has to be pulled in place. */ SETTINGS_UPDATED, - /** Settings repository was cloned from scratch (blank state). */ + /** The settings repository was cloned to a temporary directory and is valid - it has to be moved to its final location. */ SETTINGS_CLONED, - /** Settings update failed (could not clone or invalid repository). */ + /** The settings could not be updated but the settings already present are still valid so the process can continue without updating them. */ SETTINGS_UPDATE_FAILED } /** - * Result object containing the outcome and repository type. + * Result of the settings {@link SettingsUpdater#checkSettings() health check}. + * + * @param status the {@link ResultStatus}. + * @param repositoryType the {@link RepositoryType} of the settings repository. + * @param errorMessage the reason why the settings could not be updated or {@code null} if the health check succeeded. */ - public record SettingsUpdateResult(ResultStatus status, RepositoryType repositoryType) { + public record SettingsUpdateResult(ResultStatus status, RepositoryType repositoryType, String errorMessage) { + + /** + * @param status the {@link ResultStatus}. + * @param repositoryType the {@link RepositoryType}. + * @return a {@link SettingsUpdateResult} for a successful health check. + */ + static SettingsUpdateResult of(ResultStatus status, RepositoryType repositoryType) { + + return new SettingsUpdateResult(status, repositoryType, null); + } + + /** + * @param repositoryType the {@link RepositoryType} of the settings that are already present. + * @param errorMessage the reason why the settings could not be updated. + * @return a {@link SettingsUpdateResult} for a failed but recoverable health check. + */ + static SettingsUpdateResult failed(RepositoryType repositoryType, String errorMessage) { + return new SettingsUpdateResult(ResultStatus.SETTINGS_UPDATE_FAILED, repositoryType, errorMessage); + } } /** - * Creates a new SettingsUpdater. + * The constructor. * - * @param context the IDE context - * @param settingsRepoProperty the settings repository property from the update commandlet + * @param context the {@link IdeContext}. + * @param settingsRepoProperty the {@link StringProperty} with the settings repository URL from the update commandlet. */ - public SettingsUpdater(AbstractIdeContext context, StringProperty settingsRepoProperty) { + public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty) { + + super(); this.context = context; this.settingsRepoProperty = settingsRepoProperty; } /** - * Updates the settings repository by either pulling (if exists) or cloning (if new). + * Performs the health check on the settings repository. Nothing is changed in {@link IdeContext#getIdeHome() IDE_HOME} except that a broken settings folder + * is backed up. Whether the settings are pulled or cloned is decided solely by the state of {@link IdeContext#getSettingsPath() IDE_HOME/settings} so that + * {@code ide create} and {@code ide update} share the very same logic. * - * @param codeRepository whether this is a code repository (skip pull if true and not forced) - * @return the result of the settings update operation + * @return the {@link SettingsUpdateResult}. */ - public SettingsUpdateResult updateSettings(boolean codeRepository) { + public SettingsUpdateResult checkSettings() { Path settingsPath = this.context.getSettingsPath(); - boolean isSettingsRepo = this.context.getGitContext().isGitRepo(settingsPath); - - // If it's a code repository and not forced, skip the pull - if (codeRepository && isSettingsRepo && !this.context.isForceMode()) { - LOG.info("Skipping git pull in settings due to code repository. Use --force-pull to enforce pulling."); - return new SettingsUpdateResult(ResultStatus.SETTINGS_UPDATED, RepositoryType.SETTINGS); + if (settingsPath != null) { + // for a combined code and settings repository IDE_HOME/settings is a symlink into the code repository whose '.git' folder is one level above, + // so isGitRepo would report it as broken settings + boolean codeRepository = this.context.isSettingsCodeRepository(); + if (codeRepository || this.context.getGitContext().isGitRepo(settingsPath)) { + return checkPresentSettings(settingsPath, codeRepository ? RepositoryType.CODE_SETTINGS_COMBINED : RepositoryType.SETTINGS); + } } + return checkClonedSettings(settingsPath); + } - if (isSettingsRepo) { - // Existing settings repository - pull updates - return pullExistingSettings(settingsPath); - } else { - // No existing settings - clone from scratch - return cloneSettings(); + /** + * Applies the result of the {@link #checkSettings() health check} by either pulling the settings in place or moving the verified clone to its final + * location. + * + * @param result the {@link SettingsUpdateResult} from {@link #checkSettings()}. + */ + public void applySettings(SettingsUpdateResult result) { + + switch (result.status()) { + case SETTINGS_UPDATED -> pullSettings(); + case SETTINGS_CLONED -> moveSettings(result.repositoryType()); + case SETTINGS_UPDATE_FAILED -> LOG.error("Settings repository has not been updated: {}", result.errorMessage()); } } - private SettingsUpdateResult pullExistingSettings(Path settingsPath) { + /** + * Health check for settings that are already present. As the project keeps working with these settings, a failure is only fatal if the user explicitly + * aborted. + */ + private SettingsUpdateResult checkPresentSettings(Path settingsPath, RepositoryType repositoryType) { - GitContext gitContext = this.context.getGitContext(); - if (gitContext.hasUntrackedFiles(settingsPath)) { - gitContext.pullSafelyWithStash(settingsPath); - } else { - gitContext.pull(settingsPath); + try { + GitUrl gitUrl = GitUrl.of(this.context.getGitContext().retrieveGitUrl(settingsPath)); + RepositoryType clonedType = RepositoryUtil.getRepositoryType(cloneToTempDir(gitUrl)); + deleteTempDir(); + if (!isSettingsRepository(clonedType) && !confirmInvalidRepository(clonedType, gitUrl)) { + return SettingsUpdateResult.failed(repositoryType, MESSAGE_INVALID_REPOSITORY); + } + return SettingsUpdateResult.of(ResultStatus.SETTINGS_UPDATED, repositoryType); + } catch (RuntimeException e) { + deleteTempDir(); + if (e instanceof CliAbortException) { + // the user answered "no" so we must not silently carry on + throw toFatalException(e); + } + return SettingsUpdateResult.failed(repositoryType, e.getMessage()); } - this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); - return new SettingsUpdateResult(ResultStatus.SETTINGS_UPDATED, RepositoryType.SETTINGS); } - private SettingsUpdateResult cloneSettings() { + /** + * Health check for missing or broken settings. Without valid settings there is nothing to continue with, so every failure is fatal here. + */ + private SettingsUpdateResult checkClonedSettings(Path settingsPath) { - Path tempProjectPath = null; try { - // Get settings URL + backupBrokenSettings(settingsPath); GitUrl gitUrl = getOrAskSettingsUrl(); - - // Use unique temp directory to avoid leftovers from previous attempts - tempProjectPath = createUniqueTempProjectPath(); - this.context.getGitContext().pullOrClone(gitUrl, tempProjectPath); - return checkIntegrityAndMove(tempProjectPath, gitUrl.getProjectName()); - } catch (Exception e) { - // Clean up temp directory on failure - if (tempProjectPath != null) { - this.context.getFileAccess().backup(tempProjectPath); + RepositoryType repositoryType = RepositoryUtil.getRepositoryType(cloneToTempDir(gitUrl)); + if (!isSettingsRepository(repositoryType) && !confirmInvalidRepository(repositoryType, gitUrl)) { + throw new CliRethrowException(MESSAGE_INVALID_REPOSITORY); } - throw new CliRethrowException("Settings repository integrity check failed: " + e.getMessage(), e); + return SettingsUpdateResult.of(ResultStatus.SETTINGS_CLONED, repositoryType); + } catch (RuntimeException e) { + deleteTempDir(); + throw toFatalException(e); } } - private GitUrl getOrAskSettingsUrl() { + /** + * @param error the {@link RuntimeException} that made the settings setup fail. + * @return a {@link CliRethrowException} that aborts the entire process. An existing {@link CliException} keeps its message and + * {@link CliException#getExitCode() exit code} so that e.g. an abort by the user is still reported as such. + */ + private static CliRethrowException toFatalException(RuntimeException error) { - String repository = this.settingsRepoProperty.getValue(); - repository = handleDefaultRepository(repository); - String userPrompt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; - String defaultUrl = IdeContext.DEFAULT_SETTINGS_REPO_URL; - LOG.info(MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath()); + if (error instanceof CliRethrowException rethrow) { + return rethrow; + } else if (error instanceof CliException) { + return new CliRethrowException(error.getMessage(), error); + } + return new CliRethrowException("Failed to set up the settings repository: " + error.getMessage(), error); + } - GitUrl gitUrl = null; - if (repository != null) { - gitUrl = GitUrl.of(repository); + private void pullSettings() { + + Path settingsPath = this.context.getSettingsPath(); + GitContext gitContext = this.context.getGitContext(); + if (gitContext.hasUntrackedFiles(settingsPath)) { + gitContext.pullSafelyWithStash(settingsPath); + } else { + gitContext.pull(settingsPath); } - while ((gitUrl == null) || !gitUrl.isValid()) { - repository = this.context.askForInput(userPrompt, defaultUrl); - repository = handleDefaultRepository(repository); - gitUrl = GitUrl.of(repository); - if (!gitUrl.isValid()) { - LOG.warn("The input URL is not valid, please try again."); + gitContext.saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); + } + + private void moveSettings(RepositoryType repositoryType) { + + Path settingsPath = this.context.getSettingsPath(); + if ((repositoryType == RepositoryType.SETTINGS) || (repositoryType == RepositoryType.UNKNOWN)) { + moveProject(this.tempDir, settingsPath); + this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); + } else { + // for a code repository we clone into the workspace and symlink IDE_HOME/settings to its settings folder + Path codePath = this.context.getWorkspacePath().resolve(this.gitProjectName); + moveProject(this.tempDir, codePath); + Path settingsFolder = codePath.resolve(IdeContext.FOLDER_SETTINGS); + if (Files.isDirectory(settingsFolder)) { + this.context.getFileAccess().symlink(settingsFolder, settingsPath); + this.context.getGitContext().saveCurrentCommitId(settingsFolder, this.context.getSettingsCommitIdPath()); + } else { + LOG.warn("The repository has been cloned to {} but it does not contain a settings folder so your project has no settings.", codePath); } } - return gitUrl; + this.tempDir = null; } - private Path createUniqueTempProjectPath() { + private Path cloneToTempDir(GitUrl gitUrl) { - // Use FileAccess.createTempDir to ensure unique directory and avoid leftovers - FileAccess fileAccess = this.context.getFileAccess(); - Path tempProjectsDir = this.context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS); - fileAccess.mkdirs(tempProjectsDir); - return fileAccess.createTempDir(this.context.getProjectName() + "-"); + this.gitProjectName = gitUrl.getProjectName(); + // createTempDir guarantees a unique and empty directory so no leftovers of a previous attempt can interfere and we can clone directly + this.tempDir = this.context.getFileAccess().createTempDir(this.gitProjectName + "-"); + this.context.getGitContext().clone(gitUrl, this.tempDir); + return this.tempDir; } - private SettingsUpdateResult checkIntegrityAndMove(Path projectPath, String gitProjectName) { + private void backupBrokenSettings(Path settingsPath) { + if ((settingsPath == null) || !Files.exists(settingsPath)) { + return; + } FileAccess fileAccess = this.context.getFileAccess(); + if (!fileAccess.isEmptyDir(settingsPath)) { + this.context.askToContinue(""" + Your settings repository seems to be broken ('.git' folder not present). + We can fix this by moving your settings to the backup. + You will be asked for the settings git URL and your settings will be cloned from scratch. + Do you want to proceed?"""); + } + fileAccess.backup(settingsPath); + } - if (!Files.exists(projectPath)) { - throw new CliRethrowException("Git pull target folder does not exist."); + /** + * @return {@code true} if the user explicitly wants to continue with an invalid repository, {@code false} otherwise. + */ + private boolean confirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl) { + + if (!this.context.isForceMode()) { + return false; } + LOG.warn("{}\nURL: {}\nDetected repository type: {}", MESSAGE_INVALID_REPOSITORY, gitUrl, repositoryType); + this.context.askToContinue("Force mode is active. Do you want to continue anyway?"); + return true; + } - Path targetDirectory; - RepositoryType repoType = RepositoryUtil.getRepositoryType(projectPath, gitProjectName); + private static boolean isSettingsRepository(RepositoryType repositoryType) { - switch (repoType) { - case SETTINGS -> { - targetDirectory = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); - moveProject(projectPath, targetDirectory); - this.context.getGitContext().saveCurrentCommitId(targetDirectory, this.context.getSettingsCommitIdPath()); - return new SettingsUpdateResult(ResultStatus.SETTINGS_CLONED, RepositoryType.SETTINGS); - } - case CODE_SETTINGS_COMBINED -> { - // Special case: symlink from IDE_HOME/settings to workspace/repo_name/settings - targetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); - moveProject(projectPath, targetDirectory); + return (repositoryType == RepositoryType.SETTINGS) || (repositoryType == RepositoryType.CODE_SETTINGS_COMBINED); + } - Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); - Path symlinkTargetPath = this.context.getWorkspacePath().resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS); + /** + * Releases the temporary clone if it has not been moved to its final location. The clone is created by {@link #checkSettings()} and consumed by + * {@link #applySettings(SettingsUpdateResult)}, so its lifetime spans both phases and has to be ended by the caller once it is done with them. + */ + public void cleanup() { - fileAccess.symlink(symlinkTargetPath, symlinkPath); - this.context.getGitContext().saveCurrentCommitId(symlinkTargetPath, this.context.getSettingsCommitIdPath()); - return new SettingsUpdateResult(ResultStatus.SETTINGS_CLONED, RepositoryType.CODE_SETTINGS_COMBINED); - } - default -> { - fileAccess.backup(projectPath); - throw new CliRethrowException(getIntegrityCheckErrorMessage(String.format( - "The given git repository URL does not point to a valid settings or code-settings repository. " - + "Please verify and try again. Before trying again, please delete the folder %s", - this.context.getIdeHome()))); - } - } + deleteTempDir(); } - private Path moveProject(Path from, Path to) { + /** + * Removes the temporary clone. It is deleted and not backed up since it only contains a fresh clone without any user data and a backup would be created + * inside {@link IdeContext#getIdeHome() IDE_HOME} that may not even exist yet. Failures are only logged so that the actual error never gets masked. + */ + private void deleteTempDir() { - FileAccess fileAccess = this.context.getFileAccess(); + if (this.tempDir == null) { + return; + } try { - fileAccess.move(from, to); - } catch (Exception e) { - throw new CliRethrowException(String.format("Failed to move project from %s to %s", from, to), e); + this.context.getFileAccess().delete(this.tempDir); + } catch (RuntimeException e) { + LOG.warn("Failed to delete temporary directory {}", this.tempDir, e); } - return to; + this.tempDir = null; } - private String getIntegrityCheckErrorMessage(String message) { - return String.format("Settings repository integrity check failed: %s", message); + private GitUrl getOrAskSettingsUrl() { + + String repository = handleDefaultRepository(this.settingsRepoProperty.getValue()); + GitUrl gitUrl = null; + if (repository != null) { + gitUrl = GitUrl.of(repository); + } + if ((gitUrl == null) || !gitUrl.isValid()) { + LOG.info(MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath()); + } + String userPrompt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; + while ((gitUrl == null) || !gitUrl.isValid()) { + repository = handleDefaultRepository(this.context.askForInput(userPrompt, IdeContext.DEFAULT_SETTINGS_REPO_URL)); + gitUrl = GitUrl.of(repository); + if (!gitUrl.isValid()) { + LOG.warn("The input URL is not valid, please try again."); + } + } + return gitUrl; } private String handleDefaultRepository(String repository) { + if ("-".equals(repository)) { 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 moveProject(Path from, Path to) { + + try { + this.context.getFileAccess().move(from, to); + } catch (RuntimeException e) { + // FileAccess already reports source, target and the Windows file-lock hint so we only escalate to a fatal error here + throw new CliRethrowException(e.getMessage(), e); + } + } } From ce834085b5201189bb2e695114cd6ccfba9d1090 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 23:06:53 +0200 Subject: [PATCH 29/89] #1695: cleanup --- .../update/AbstractUpdateCommandlet.java | 8 ++++---- .../ide/commandlet/update/SettingsUpdater.java | 17 ++++------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 725254c3df..ed914ece7b 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -174,24 +174,24 @@ protected void updateSettings() { protected String getStepMessage() { - return "update (pull) settings repository"; + return "Update settings repository"; } private void updateSettingsInStep(Step step) { SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo); try { - SettingsUpdateResult result = this.context.newStep("Performing health check on settings").call(settingsUpdater::checkSettings, () -> null); + SettingsUpdateResult result = this.context.newStep("Performing settings health check").call(settingsUpdater::checkSettings, () -> null); // fatal problems (e.g. no valid settings at all) were already rethrown, so reaching this point means the settings we have stay usable if (result == null) { - step.error("Health check on settings failed - the settings have not been updated."); + step.error("Health check on settings failed due to unknown error - the settings have not been updated."); return; } else if (result.status() == ResultStatus.SETTINGS_UPDATE_FAILED) { step.error("The settings have not been updated: {}", result.errorMessage()); return; } prepareProject(); - boolean applied = this.context.newStep("Applying update").run(() -> settingsUpdater.applySettings(result)); + boolean applied = this.context.newStep("Applying settings").run(() -> settingsUpdater.applySettings(result)); if (!applied) { step.error("Failed to apply the settings update."); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java index c64c7de9e4..24a883d6be 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java @@ -150,13 +150,13 @@ private SettingsUpdateResult checkPresentSettings(Path settingsPath, RepositoryT try { GitUrl gitUrl = GitUrl.of(this.context.getGitContext().retrieveGitUrl(settingsPath)); RepositoryType clonedType = RepositoryUtil.getRepositoryType(cloneToTempDir(gitUrl)); - deleteTempDir(); + cleanup(); if (!isSettingsRepository(clonedType) && !confirmInvalidRepository(clonedType, gitUrl)) { return SettingsUpdateResult.failed(repositoryType, MESSAGE_INVALID_REPOSITORY); } return SettingsUpdateResult.of(ResultStatus.SETTINGS_UPDATED, repositoryType); } catch (RuntimeException e) { - deleteTempDir(); + cleanup(); if (e instanceof CliAbortException) { // the user answered "no" so we must not silently carry on throw toFatalException(e); @@ -179,7 +179,7 @@ private SettingsUpdateResult checkClonedSettings(Path settingsPath) { } return SettingsUpdateResult.of(ResultStatus.SETTINGS_CLONED, repositoryType); } catch (RuntimeException e) { - deleteTempDir(); + cleanup(); throw toFatalException(e); } } @@ -275,20 +275,11 @@ private static boolean isSettingsRepository(RepositoryType repositoryType) { return (repositoryType == RepositoryType.SETTINGS) || (repositoryType == RepositoryType.CODE_SETTINGS_COMBINED); } - /** - * Releases the temporary clone if it has not been moved to its final location. The clone is created by {@link #checkSettings()} and consumed by - * {@link #applySettings(SettingsUpdateResult)}, so its lifetime spans both phases and has to be ended by the caller once it is done with them. - */ - public void cleanup() { - - deleteTempDir(); - } - /** * Removes the temporary clone. It is deleted and not backed up since it only contains a fresh clone without any user data and a backup would be created * inside {@link IdeContext#getIdeHome() IDE_HOME} that may not even exist yet. Failures are only logged so that the actual error never gets masked. */ - private void deleteTempDir() { + public void cleanup() { if (this.tempDir == null) { return; From fce29f7b6d4760330c309b9634e5c1c1afe39333 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 23:24:39 +0200 Subject: [PATCH 30/89] #1695: spotless apply --- .../tools/ide/commandlet/update/AbstractUpdateCommandlet.java | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index ed914ece7b..36f37d10e7 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -182,6 +182,7 @@ private void updateSettingsInStep(Step step) { SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo); try { SettingsUpdateResult result = this.context.newStep("Performing settings health check").call(settingsUpdater::checkSettings, () -> null); + // fatal problems (e.g. no valid settings at all) were already rethrown, so reaching this point means the settings we have stay usable if (result == null) { step.error("Health check on settings failed due to unknown error - the settings have not been updated."); From ca59b00c067820e254540d28663d4a3ee536d6df Mon Sep 17 00:00:00 2001 From: laim2003 Date: Thu, 27 Aug 2026 14:03:29 +0200 Subject: [PATCH 31/89] #1695: finalized verification workflow when no settings repo exists yet; update workflow WIP --- ...wException.java => CliFatalException.java} | 6 +- .../ide/commandlet/CreateCommandlet.java | 2 +- .../ide/commandlet/StatusCommandlet.java | 2 +- .../update/AbstractUpdateCommandlet.java | 60 ++++-- .../settings/HealthCheckResultStatus.java | 14 ++ .../settings/SettingsHealthCheckResult.java | 35 ++++ .../update/settings/SettingsUpdateResult.java | 7 + .../update/settings/SettingsUpdateStatus.java | 11 + .../{ => settings}/SettingsUpdater.java | 192 +++++++++--------- .../tools/ide/context/AbstractIdeContext.java | 6 +- .../devonfw/tools/ide/context/IdeContext.java | 2 +- .../com/devonfw/tools/ide/step/StepTest.java | 10 +- 12 files changed, 221 insertions(+), 126 deletions(-) rename cli/src/main/java/com/devonfw/tools/ide/cli/{CliRethrowException.java => CliFatalException.java} (83%) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateResult.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateStatus.java rename cli/src/main/java/com/devonfw/tools/ide/commandlet/update/{ => settings}/SettingsUpdater.java (61%) diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliFatalException.java similarity index 83% rename from cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java rename to cli/src/main/java/com/devonfw/tools/ide/cli/CliFatalException.java index fc991233bd..11a153aa76 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliFatalException.java @@ -5,14 +5,14 @@ * settings repository). Unlike a regular error that only makes the current {@link com.devonfw.tools.ide.step.Step Step} fail while the overall process * continues, this exception {@link #isForceRethrowInStep() is always re-thrown} so no further step is executed in an invalid state. */ -public final class CliRethrowException extends CliException { +public final class CliFatalException extends CliException { /** * The constructor. * * @param message the {@link #getMessage() message}. */ - public CliRethrowException(String message) { + public CliFatalException(String message) { super(message); } @@ -23,7 +23,7 @@ public CliRethrowException(String message) { * @param message the {@link #getMessage() message}. * @param cause the {@link #getCause() cause}. */ - public CliRethrowException(String message, Throwable cause) { + public CliFatalException(String message, Throwable cause) { super(message, cause); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index 67e2256983..e5c5b24b82 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -68,7 +68,7 @@ protected void doRun() { } @Override - protected void prepareProject() { + protected void onSettingHealthCheckSucceeded() { // only called after the settings passed the health check Path newProjectPath = getNewProjectPath(); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java index 39a5131ff5..1cd9fe2fcd 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java @@ -104,7 +104,7 @@ private void logSettingsGitStatus() { } else { GitContext gitContext = this.context.getGitContext(); if (gitContext.isRepositoryUpdateAvailable(settingsPath, this.context.getSettingsCommitIdPath())) { - if (!this.context.isSettingsCodeRepository()) { + if (!this.context.isCombinedSettingsCodeRepository()) { LOG.warn("Your settings are not up-to-date, please run 'ide update'."); } } else { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 36f37d10e7..5019e54430 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -9,14 +9,17 @@ import java.util.Set; import java.util.stream.Stream; +import com.devonfw.tools.ide.commandlet.update.settings.HealthCheckResultStatus; +import com.devonfw.tools.ide.commandlet.update.settings.SettingsHealthCheckResult; +import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdateResult; +import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdater; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.devonfw.tools.ide.commandlet.Commandlet; import com.devonfw.tools.ide.commandlet.CommandletManager; import com.devonfw.tools.ide.commandlet.CreateCommandlet; -import com.devonfw.tools.ide.commandlet.update.SettingsUpdater.ResultStatus; -import com.devonfw.tools.ide.commandlet.update.SettingsUpdater.SettingsUpdateResult; import com.devonfw.tools.ide.context.AbstractIdeContext; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.context.IdeStartContextImpl; @@ -102,7 +105,7 @@ protected void doRun() { * Hook that is called after the settings passed the health check but before they are moved to their final location. Does nothing by default and is overridden * by {@link CreateCommandlet} to create the project structure so that no project is created at all if the health check failed. */ - protected void prepareProject() { + protected void onSettingHealthCheckSucceeded() { // nothing to do by default } @@ -163,7 +166,7 @@ private void setupConf(Path template, Path conf) { */ protected void updateSettings() { - boolean codeRepository = this.context.isSettingsCodeRepository(); + boolean codeRepository = this.context.isCombinedSettingsCodeRepository(); if (codeRepository && !(this.context.isForceMode() || this.forcePull.isTrue())) { LOG.info("Skipping git pull in settings due to code repository. Use --force-pull to enforce pulling."); return; @@ -181,21 +184,42 @@ private void updateSettingsInStep(Step step) { SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo); try { - SettingsUpdateResult result = this.context.newStep("Performing settings health check").call(settingsUpdater::checkSettings, () -> null); + //Step 1: Perform health check + Step healthCheckStep = this.context.newStep("Performing settings health check"); + Path temporaryRepoDir = healthCheckStep.call(() -> { + SettingsHealthCheckResult healthCheckResult = settingsUpdater.checkSettings(this.context.getSettingsPath()); + HealthCheckResultStatus status = healthCheckResult.status(); + + if (status == null) { + healthCheckStep.error("Health check on settings failed due to unknown error - the settings have not been updated."); + return healthCheckResult.temporarySettingsDirectory(); + } else if (healthCheckResult.status() == HealthCheckResultStatus.SETTINGS_INVALID) { + healthCheckStep.error("The settings have not been updated: {}", healthCheckResult.errorMessage()); + return healthCheckResult.temporarySettingsDirectory(); + } + return healthCheckResult.temporarySettingsDirectory(); + }, () -> null); + if(temporaryRepoDir == null || healthCheckStep.isFailure()) return; - // fatal problems (e.g. no valid settings at all) were already rethrown, so reaching this point means the settings we have stay usable - if (result == null) { - step.error("Health check on settings failed due to unknown error - the settings have not been updated."); - return; - } else if (result.status() == ResultStatus.SETTINGS_UPDATE_FAILED) { - step.error("The settings have not been updated: {}", result.errorMessage()); - return; - } - prepareProject(); - boolean applied = this.context.newStep("Applying settings").run(() -> settingsUpdater.applySettings(result)); - if (!applied) { - step.error("Failed to apply the settings update."); - } + //Step 2: Let create/update commandlets prepare themselves for the settings update. + onSettingHealthCheckSucceeded(); + + //Step 3: Apply (move/pull newest version) settings + Step applySettingsStep = this.context.newStep("Applying settings"); + applySettingsStep.run(() -> { + SettingsUpdateResult settingsUpdateResult = settingsUpdater.applySettings(temporaryRepoDir); + + if (settingsUpdateResult == null) { + applySettingsStep.error("Failed to apply the settings update due to unknown error."); + return; + } + switch (settingsUpdateResult.updateStatus()) { + case SETTINGS_UPDATED -> applySettingsStep.success("Settings update successfully applied"); + case SETTINGS_CLONED -> applySettingsStep.success("Settings successfully applied (cloned)"); + case SETTINGS_UPDATE_FAILED -> applySettingsStep.error("The settings update could not be applied: {}", settingsUpdateResult.errorMessage()); + case null, default -> applySettingsStep.error("Unexpected value: {}", settingsUpdateResult.updateStatus()); + } + }); } finally { // the verified clone lives across both steps and the prepareProject hook so it is only here that its lifetime ends settingsUpdater.cleanup(); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java new file mode 100644 index 0000000000..673ef4f00b --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java @@ -0,0 +1,14 @@ +package com.devonfw.tools.ide.commandlet.update.settings; + +/** + * Status of the settings {@link SettingsUpdater#checkSettings() health check} describing what {@link SettingsUpdater#applySettings(SettingsHealthCheckResult)} has + * to do. + */ +public enum HealthCheckResultStatus { + /** The settings repository was cloned to a temporary directory and is valid - it can be moved to its final location. */ + SETTINGS_VALID, + /** The settings repository already existed and was cloned to a temporary directory and is valid - it can be moved to its final location. */ + SETTINGS_VALID_EXISTING, + /** The settings repository is invalid */ + SETTINGS_INVALID +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java new file mode 100644 index 0000000000..e28692c112 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java @@ -0,0 +1,35 @@ +package com.devonfw.tools.ide.commandlet.update.settings; + +import com.devonfw.tools.ide.git.repository.RepositoryType; + +import java.nio.file.Path; + +/** + * Result of the settings {@link SettingsUpdater#checkSettings() health check}. + * + * @param status the {@link HealthCheckResultStatus}. + * @param repositoryType the {@link RepositoryType} of the settings repository. + * @param errorMessage the reason why the settings could not be updated or {@code null} if the health check succeeded. + */ +public record SettingsHealthCheckResult(HealthCheckResultStatus status, RepositoryType repositoryType, Path temporarySettingsDirectory, String errorMessage) { + + /** + * @param status the {@link HealthCheckResultStatus}. + * @param repositoryType the {@link RepositoryType}. + * @return a {@link SettingsHealthCheckResult} for a successful health check. + */ + public static SettingsHealthCheckResult of(HealthCheckResultStatus status, RepositoryType repositoryType, Path temporaryRepoDirectory) { + + return new SettingsHealthCheckResult(status, repositoryType, temporaryRepoDirectory, null); + } + + /** + * @param repositoryType the {@link RepositoryType} of the settings that are already present. + * @param errorMessage the reason why the settings could not be updated. + * @return a {@link SettingsHealthCheckResult} for a failed but recoverable health check. + */ + public static SettingsHealthCheckResult failed(RepositoryType repositoryType, String errorMessage, Path temporaryRepoDirectory) { + + return new SettingsHealthCheckResult(HealthCheckResultStatus.SETTINGS_INVALID, repositoryType, temporaryRepoDirectory, errorMessage); + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateResult.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateResult.java new file mode 100644 index 0000000000..27b82b2d4c --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateResult.java @@ -0,0 +1,7 @@ +package com.devonfw.tools.ide.commandlet.update.settings; + +import com.devonfw.tools.ide.git.repository.RepositoryType; + +public record SettingsUpdateResult(SettingsUpdateStatus updateStatus, RepositoryType repositoryType, String errorMessage) { + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateStatus.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateStatus.java new file mode 100644 index 0000000000..d13871f4eb --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateStatus.java @@ -0,0 +1,11 @@ +package com.devonfw.tools.ide.commandlet.update.settings; + +/// Status of the update action of a settings repo. +public enum SettingsUpdateStatus { + /** Existing settings have been successfully updated **/ + SETTINGS_UPDATED, + /** Freshly cloned settings have been successfully applied **/ + SETTINGS_CLONED, + /** Error occurred **/ + SETTINGS_UPDATE_FAILED +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java similarity index 61% rename from cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java rename to cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index 24a883d6be..6df8c24f9a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -1,4 +1,4 @@ -package com.devonfw.tools.ide.commandlet.update; +package com.devonfw.tools.ide.commandlet.update.settings; import java.nio.file.Files; import java.nio.file.Path; @@ -8,7 +8,7 @@ import com.devonfw.tools.ide.cli.CliAbortException; import com.devonfw.tools.ide.cli.CliException; -import com.devonfw.tools.ide.cli.CliRethrowException; +import com.devonfw.tools.ide.cli.CliFatalException; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.git.GitContext; import com.devonfw.tools.ide.git.GitUrl; @@ -22,7 +22,7 @@ *
    *
  1. {@link #checkSettings() health check}: the settings are always cloned into a temporary directory first where it is verified that the git URL is valid, * that cloning succeeded, and that the repository actually is a settings or a combined code and settings repository.
  2. - *
  3. {@link #applySettings(SettingsUpdateResult) apply}: only after the health check succeeded the settings are either pulled in place (if they were already + *
  4. {@link #applySettings(SettingsHealthCheckResult) apply}: only after the health check succeeded the settings are either pulled in place (if they were already * present) or the verified clone is moved to its final location.
  5. *
*/ @@ -41,57 +41,16 @@ public class SettingsUpdater { private final IdeContext context; + private final FileAccess fileAccess; + private final StringProperty settingsRepoProperty; /** The temporary directory holding the verified clone or {@code null} if there is nothing to move. */ - private Path tempDir; + private Path tempRepoDir; /** The name of the git project - required to place a combined code and settings repository into the workspace. */ private String gitProjectName; - /** - * Status of the settings {@link SettingsUpdater#checkSettings() health check} describing what {@link SettingsUpdater#applySettings(SettingsUpdateResult)} has - * to do. - */ - public enum ResultStatus { - /** The settings repository was already present and is valid - it only has to be pulled in place. */ - SETTINGS_UPDATED, - /** The settings repository was cloned to a temporary directory and is valid - it has to be moved to its final location. */ - SETTINGS_CLONED, - /** The settings could not be updated but the settings already present are still valid so the process can continue without updating them. */ - SETTINGS_UPDATE_FAILED - } - - /** - * Result of the settings {@link SettingsUpdater#checkSettings() health check}. - * - * @param status the {@link ResultStatus}. - * @param repositoryType the {@link RepositoryType} of the settings repository. - * @param errorMessage the reason why the settings could not be updated or {@code null} if the health check succeeded. - */ - public record SettingsUpdateResult(ResultStatus status, RepositoryType repositoryType, String errorMessage) { - - /** - * @param status the {@link ResultStatus}. - * @param repositoryType the {@link RepositoryType}. - * @return a {@link SettingsUpdateResult} for a successful health check. - */ - static SettingsUpdateResult of(ResultStatus status, RepositoryType repositoryType) { - - return new SettingsUpdateResult(status, repositoryType, null); - } - - /** - * @param repositoryType the {@link RepositoryType} of the settings that are already present. - * @param errorMessage the reason why the settings could not be updated. - * @return a {@link SettingsUpdateResult} for a failed but recoverable health check. - */ - static SettingsUpdateResult failed(RepositoryType repositoryType, String errorMessage) { - - return new SettingsUpdateResult(ResultStatus.SETTINGS_UPDATE_FAILED, repositoryType, errorMessage); - } - } - /** * The constructor. * @@ -103,6 +62,7 @@ public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty) super(); this.context = context; this.settingsRepoProperty = settingsRepoProperty; + this.fileAccess = context.getFileAccess(); } /** @@ -110,17 +70,16 @@ public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty) * is backed up. Whether the settings are pulled or cloned is decided solely by the state of {@link IdeContext#getSettingsPath() IDE_HOME/settings} so that * {@code ide create} and {@code ide update} share the very same logic. * - * @return the {@link SettingsUpdateResult}. + * @return the {@link SettingsHealthCheckResult}. */ - public SettingsUpdateResult checkSettings() { + public SettingsHealthCheckResult checkSettings(Path settingsPath) { - Path settingsPath = this.context.getSettingsPath(); - if (settingsPath != null) { + if (settingsPath != null && !fileAccess.isEmptyDir(settingsPath)) { // for a combined code and settings repository IDE_HOME/settings is a symlink into the code repository whose '.git' folder is one level above, // so isGitRepo would report it as broken settings - boolean codeRepository = this.context.isSettingsCodeRepository(); - if (codeRepository || this.context.getGitContext().isGitRepo(settingsPath)) { - return checkPresentSettings(settingsPath, codeRepository ? RepositoryType.CODE_SETTINGS_COMBINED : RepositoryType.SETTINGS); + RepositoryType settingsRepoType = RepositoryUtil.getRepositoryType(settingsPath); + if (isSettingsOrCodeSettingsRepository(settingsRepoType)) { + return checkSettingsPresent(settingsPath, settingsRepoType); } } return checkClonedSettings(settingsPath); @@ -130,75 +89,114 @@ public SettingsUpdateResult checkSettings() { * Applies the result of the {@link #checkSettings() health check} by either pulling the settings in place or moving the verified clone to its final * location. * - * @param result the {@link SettingsUpdateResult} from {@link #checkSettings()}. + * @param sourcePath sourcePath of the settings to apply. + * @return a {@link SettingsUpdateResult} representing the state, whether moving/pulling the newest settings was successful. */ - public void applySettings(SettingsUpdateResult result) { + public SettingsUpdateResult applySettings(Path sourcePath) { + + RepositoryType repositoryType = RepositoryUtil.getRepositoryType(sourcePath); - switch (result.status()) { - case SETTINGS_UPDATED -> pullSettings(); - case SETTINGS_CLONED -> moveSettings(result.repositoryType()); - case SETTINGS_UPDATE_FAILED -> LOG.error("Settings repository has not been updated: {}", result.errorMessage()); + switch (repositoryType) { + case CODE -> { + //Technically should be caught during a health check, but we still handle this here. + + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, MESSAGE_INVALID_REPOSITORY); + } + case SETTINGS -> { + //move to IDE_HOME/SETTINGS + + moveProject(sourcePath, context.getSettingsPath()); + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); + } + case CODE_SETTINGS_COMBINED -> { + //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. (Formerly managed by the obsolete "--code" flag) + Path targetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); + moveProject(sourcePath, targetDirectory); + + Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); + Path symlinkTargetPath = this.context.getWorkspacePath().resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS); + + context.getFileAccess().symlink(symlinkTargetPath, symlinkPath); + this.context.getGitContext().saveCurrentCommitId(symlinkTargetPath, this.context.getSettingsCommitIdPath()); + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); + } + case UNKNOWN -> { + + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, MESSAGE_INVALID_REPOSITORY); + } } + + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Unknown error during settings"); } /** * Health check for settings that are already present. As the project keeps working with these settings, a failure is only fatal if the user explicitly - * aborted. + * aborted. Here, if a new version is available, we clone the new version into a temporary folder and perform health checks. If the cloned, new version is valid, + * we call git update in the existing settings folder. */ - private SettingsUpdateResult checkPresentSettings(Path settingsPath, RepositoryType repositoryType) { + private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, RepositoryType repositoryType) { try { + //Get Git url of existing settings, clone newest version of them to temp dir GitUrl gitUrl = GitUrl.of(this.context.getGitContext().retrieveGitUrl(settingsPath)); - RepositoryType clonedType = RepositoryUtil.getRepositoryType(cloneToTempDir(gitUrl)); + RepositoryType clonedType = RepositoryUtil.getRepositoryType(cloneRepoToTempDir(gitUrl)); cleanup(); - if (!isSettingsRepository(clonedType) && !confirmInvalidRepository(clonedType, gitUrl)) { - return SettingsUpdateResult.failed(repositoryType, MESSAGE_INVALID_REPOSITORY); + + //If cloned repo is not (code-)settings repo and no force override (e.g. force mode) is applied, return error. + if (!isSettingsOrCodeSettingsRepository(clonedType) && !requestUserConfirmInvalidRepository(clonedType, gitUrl)) { + return SettingsHealthCheckResult.failed(clonedType, MESSAGE_INVALID_REPOSITORY, settingsPath); } - return SettingsUpdateResult.of(ResultStatus.SETTINGS_UPDATED, repositoryType); + + //Otherwise, (e.g. user overrides), return valid. + return SettingsHealthCheckResult.of(HealthCheckResultStatus.SETTINGS_VALID, repositoryType, settingsPath); } catch (RuntimeException e) { cleanup(); if (e instanceof CliAbortException) { // the user answered "no" so we must not silently carry on - throw toFatalException(e); + throw createGuaranteedFatalException(e); } - return SettingsUpdateResult.failed(repositoryType, e.getMessage()); + return SettingsHealthCheckResult.failed(repositoryType, e.getMessage(), settingsPath); } } /** * Health check for missing or broken settings. Without valid settings there is nothing to continue with, so every failure is fatal here. */ - private SettingsUpdateResult checkClonedSettings(Path settingsPath) { + private SettingsHealthCheckResult checkClonedSettings(Path settingsPath) { try { backupBrokenSettings(settingsPath); GitUrl gitUrl = getOrAskSettingsUrl(); - RepositoryType repositoryType = RepositoryUtil.getRepositoryType(cloneToTempDir(gitUrl)); - if (!isSettingsRepository(repositoryType) && !confirmInvalidRepository(repositoryType, gitUrl)) { - throw new CliRethrowException(MESSAGE_INVALID_REPOSITORY); + + Path tempCloneDir = cloneRepoToTempDir(gitUrl); + RepositoryType repositoryType = RepositoryUtil.getRepositoryType(tempCloneDir); + if (!isSettingsOrCodeSettingsRepository(repositoryType) && !requestUserConfirmInvalidRepository(repositoryType, gitUrl)) { + //see @javadoc why we throw fatally here. + throw new CliFatalException(MESSAGE_INVALID_REPOSITORY); } - return SettingsUpdateResult.of(ResultStatus.SETTINGS_CLONED, repositoryType); + return SettingsHealthCheckResult.of(HealthCheckResultStatus.SETTINGS_VALID, repositoryType, tempCloneDir); } catch (RuntimeException e) { cleanup(); - throw toFatalException(e); + throw createGuaranteedFatalException(e); } } /** * @param error the {@link RuntimeException} that made the settings setup fail. - * @return a {@link CliRethrowException} that aborts the entire process. An existing {@link CliException} keeps its message and + * @return a {@link CliFatalException} that aborts the entire process. An existing {@link CliException} keeps its message and * {@link CliException#getExitCode() exit code} so that e.g. an abort by the user is still reported as such. */ - private static CliRethrowException toFatalException(RuntimeException error) { + private static CliFatalException createGuaranteedFatalException(RuntimeException error) { - if (error instanceof CliRethrowException rethrow) { + if (error instanceof CliFatalException rethrow) { return rethrow; } else if (error instanceof CliException) { - return new CliRethrowException(error.getMessage(), error); + return new CliFatalException(error.getMessage(), error); } - return new CliRethrowException("Failed to set up the settings repository: " + error.getMessage(), error); + return new CliFatalException("Failed to set up the settings repository: " + error.getMessage(), error); } + //TODO: Reimplement this! private void pullSettings() { Path settingsPath = this.context.getSettingsPath(); @@ -215,12 +213,12 @@ private void moveSettings(RepositoryType repositoryType) { Path settingsPath = this.context.getSettingsPath(); if ((repositoryType == RepositoryType.SETTINGS) || (repositoryType == RepositoryType.UNKNOWN)) { - moveProject(this.tempDir, settingsPath); + moveProject(this.tempRepoDir, settingsPath); this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); } else { // for a code repository we clone into the workspace and symlink IDE_HOME/settings to its settings folder Path codePath = this.context.getWorkspacePath().resolve(this.gitProjectName); - moveProject(this.tempDir, codePath); + moveProject(this.tempRepoDir, codePath); Path settingsFolder = codePath.resolve(IdeContext.FOLDER_SETTINGS); if (Files.isDirectory(settingsFolder)) { this.context.getFileAccess().symlink(settingsFolder, settingsPath); @@ -229,16 +227,22 @@ private void moveSettings(RepositoryType repositoryType) { LOG.warn("The repository has been cloned to {} but it does not contain a settings folder so your project has no settings.", codePath); } } - this.tempDir = null; + this.tempRepoDir = null; } - private Path cloneToTempDir(GitUrl gitUrl) { + /** + * Clone a settings repository into a temporary directory. + * @param gitUrl {@link GitUrl} of the (code-)settings repository. + * @return {@link Path} of the temporary directory. + */ + private Path cloneRepoToTempDir(GitUrl gitUrl) { this.gitProjectName = gitUrl.getProjectName(); + // createTempDir guarantees a unique and empty directory so no leftovers of a previous attempt can interfere and we can clone directly - this.tempDir = this.context.getFileAccess().createTempDir(this.gitProjectName + "-"); - this.context.getGitContext().clone(gitUrl, this.tempDir); - return this.tempDir; + this.tempRepoDir = this.context.getFileAccess().createTempDir("project-"+this.gitProjectName); + this.context.getGitContext().clone(gitUrl, this.tempRepoDir); + return this.tempRepoDir; } private void backupBrokenSettings(Path settingsPath) { @@ -246,7 +250,7 @@ private void backupBrokenSettings(Path settingsPath) { if ((settingsPath == null) || !Files.exists(settingsPath)) { return; } - FileAccess fileAccess = this.context.getFileAccess(); + if (!fileAccess.isEmptyDir(settingsPath)) { this.context.askToContinue(""" Your settings repository seems to be broken ('.git' folder not present). @@ -260,7 +264,7 @@ Your settings repository seems to be broken ('.git' folder not present). /** * @return {@code true} if the user explicitly wants to continue with an invalid repository, {@code false} otherwise. */ - private boolean confirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl) { + private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl) { if (!this.context.isForceMode()) { return false; @@ -270,7 +274,7 @@ private boolean confirmInvalidRepository(RepositoryType repositoryType, GitUrl g return true; } - private static boolean isSettingsRepository(RepositoryType repositoryType) { + private static boolean isSettingsOrCodeSettingsRepository(RepositoryType repositoryType) { return (repositoryType == RepositoryType.SETTINGS) || (repositoryType == RepositoryType.CODE_SETTINGS_COMBINED); } @@ -281,15 +285,15 @@ private static boolean isSettingsRepository(RepositoryType repositoryType) { */ public void cleanup() { - if (this.tempDir == null) { + if (this.tempRepoDir == null) { return; } try { - this.context.getFileAccess().delete(this.tempDir); + this.context.getFileAccess().delete(this.tempRepoDir); } catch (RuntimeException e) { - LOG.warn("Failed to delete temporary directory {}", this.tempDir, e); + LOG.warn("Failed to delete temporary directory {}", this.tempRepoDir, e); } - this.tempDir = null; + this.tempRepoDir = null; } private GitUrl getOrAskSettingsUrl() { @@ -328,7 +332,7 @@ private void moveProject(Path from, Path to) { this.context.getFileAccess().move(from, to); } catch (RuntimeException e) { // FileAccess already reports source, target and the Windows file-lock hint so we only escalate to a fatal error here - throw new CliRethrowException(e.getMessage(), e); + throw new CliFatalException(e.getMessage(), e); } } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java index 0b430e4dcb..305f175dc0 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java @@ -686,7 +686,7 @@ public Path getSettingsGitRepository() { Path settingsPath = getSettingsPath(); // check whether the settings path has a .git folder only if its not a symbolic link or junction - if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsCodeRepository()) { + if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isCombinedSettingsCodeRepository()) { LOG.error("Settings repository exists but is not a git repository."); return null; } @@ -694,7 +694,7 @@ public Path getSettingsGitRepository() { } @Override - public boolean isSettingsCodeRepository() { + public boolean isCombinedSettingsCodeRepository() { Path settingsPath = getSettingsPath(); if (settingsPath != null) { @@ -1481,7 +1481,7 @@ settingsRepository, getSettingsCommitIdPath()))) { */ private String determineSettingsUpdateMessage(Commandlet cmd) { boolean update = cmd instanceof UpdateCommandlet; - if (isSettingsCodeRepository()) { + if (isCombinedSettingsCodeRepository()) { if (update && (isForceMode() || isForcePull())) { return null; } diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java index 013591e2a8..e22cf3cd6b 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java @@ -597,7 +597,7 @@ default Path getRepositoriesPath() { /** * @return {@code true} if the settings repository is a symlink or a junction to a code-repository. */ - boolean isSettingsCodeRepository(); + boolean isCombinedSettingsCodeRepository(); /** * @return the {@link Path} to the file containing the last tracked commit Id of the settings repository. diff --git a/cli/src/test/java/com/devonfw/tools/ide/step/StepTest.java b/cli/src/test/java/com/devonfw/tools/ide/step/StepTest.java index bbc4d9332f..c4ff3587b5 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/step/StepTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/step/StepTest.java @@ -2,7 +2,7 @@ import org.junit.jupiter.api.Test; -import com.devonfw.tools.ide.cli.CliRethrowException; +import com.devonfw.tools.ide.cli.CliFatalException; import com.devonfw.tools.ide.context.AbstractIdeContextTest; import com.devonfw.tools.ide.context.IdeTestContext; import com.devonfw.tools.ide.log.IdeLogEntry; @@ -154,8 +154,8 @@ void testRunRethrowsForcedError() { Step step = context.newStep("Test-Step"); // act & assert assertThatThrownBy(() -> step.run(() -> { - throw new CliRethrowException("fatal error"); - })).isInstanceOf(CliRethrowException.class).hasMessage("fatal error"); + throw new CliFatalException("fatal error"); + })).isInstanceOf(CliFatalException.class).hasMessage("fatal error"); assertThat(step.isFailure()).isTrue(); } @@ -182,8 +182,8 @@ void testCallRethrowsForcedError() { Step step = context.newStep("Test-Step"); // act & assert assertThatThrownBy(() -> step.call(() -> { - throw new CliRethrowException("fatal error"); - }, () -> "fallback")).isInstanceOf(CliRethrowException.class).hasMessage("fatal error"); + throw new CliFatalException("fatal error"); + }, () -> "fallback")).isInstanceOf(CliFatalException.class).hasMessage("fatal error"); assertThat(step.isFailure()).isTrue(); } From 0cb06db9127866b0ad735167d66d8ba5288569c0 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Thu, 27 Aug 2026 14:11:07 +0200 Subject: [PATCH 32/89] #1695: added warning for invalid Git urls provided via a parameter. --- .../tools/ide/commandlet/update/settings/SettingsUpdater.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index 6df8c24f9a..c7ca0d43a8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -3,6 +3,7 @@ import java.nio.file.Files; import java.nio.file.Path; +import org.jline.utils.Log; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -308,6 +309,7 @@ private GitUrl getOrAskSettingsUrl() { } String userPrompt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; while ((gitUrl == null) || !gitUrl.isValid()) { + LOG.warn("The provided git url parameter {} was detected to be invalid. Please enter a valid settings url.", gitUrl); repository = handleDefaultRepository(this.context.askForInput(userPrompt, IdeContext.DEFAULT_SETTINGS_REPO_URL)); gitUrl = GitUrl.of(repository); if (!gitUrl.isValid()) { From affe458b4a17d64cdb8556f3e9743557c5941eca Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 28 Aug 2026 13:34:36 +0200 Subject: [PATCH 33/89] #1695: small fixes --- .../update/AbstractUpdateCommandlet.java | 32 ++++++++++--------- .../update/settings/SettingsUpdateResult.java | 9 ++++-- .../ide/git/repository/RepositoryUtil.java | 2 +- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 5019e54430..570688b53a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -9,11 +9,13 @@ import java.util.Set; import java.util.stream.Stream; +import com.devonfw.tools.ide.cli.CliFatalException; import com.devonfw.tools.ide.commandlet.update.settings.HealthCheckResultStatus; import com.devonfw.tools.ide.commandlet.update.settings.SettingsHealthCheckResult; import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdateResult; import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdater; +import org.jline.utils.Log; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -186,20 +188,21 @@ private void updateSettingsInStep(Step step) { try { //Step 1: Perform health check Step healthCheckStep = this.context.newStep("Performing settings health check"); - Path temporaryRepoDir = healthCheckStep.call(() -> { - SettingsHealthCheckResult healthCheckResult = settingsUpdater.checkSettings(this.context.getSettingsPath()); - HealthCheckResultStatus status = healthCheckResult.status(); + SettingsHealthCheckResult healthCheckResult; + healthCheckResult = healthCheckStep.call(() -> { + SettingsHealthCheckResult _healthCheckResult = settingsUpdater.checkSettings(this.context.getSettingsPath()); + HealthCheckResultStatus status = _healthCheckResult.status(); if (status == null) { - healthCheckStep.error("Health check on settings failed due to unknown error - the settings have not been updated."); - return healthCheckResult.temporarySettingsDirectory(); - } else if (healthCheckResult.status() == HealthCheckResultStatus.SETTINGS_INVALID) { - healthCheckStep.error("The settings have not been updated: {}", healthCheckResult.errorMessage()); - return healthCheckResult.temporarySettingsDirectory(); + throw new CliFatalException("Health check on settings failed due to unknown error - the settings have not been updated"); + } else if (status == HealthCheckResultStatus.SETTINGS_INVALID) { + throw new CliFatalException("The settings could not be updated: " + _healthCheckResult.errorMessage()); } - return healthCheckResult.temporarySettingsDirectory(); + return _healthCheckResult; }, () -> null); - if(temporaryRepoDir == null || healthCheckStep.isFailure()) return; + if (healthCheckResult == null) { + throw new CliFatalException("Health check on settings failed due to unknown error - the settings have not been updated"); + } //Step 2: Let create/update commandlets prepare themselves for the settings update. onSettingHealthCheckSucceeded(); @@ -207,17 +210,16 @@ private void updateSettingsInStep(Step step) { //Step 3: Apply (move/pull newest version) settings Step applySettingsStep = this.context.newStep("Applying settings"); applySettingsStep.run(() -> { - SettingsUpdateResult settingsUpdateResult = settingsUpdater.applySettings(temporaryRepoDir); + SettingsUpdateResult settingsUpdateResult = settingsUpdater.applySettings(healthCheckResult.status() == HealthCheckResultStatus.SETTINGS_VALID_EXISTING, + healthCheckResult.temporarySettingsDirectory()); if (settingsUpdateResult == null) { - applySettingsStep.error("Failed to apply the settings update due to unknown error."); - return; + throw new CliFatalException("Failed to apply the settings update due to unknown error."); } switch (settingsUpdateResult.updateStatus()) { case SETTINGS_UPDATED -> applySettingsStep.success("Settings update successfully applied"); case SETTINGS_CLONED -> applySettingsStep.success("Settings successfully applied (cloned)"); - case SETTINGS_UPDATE_FAILED -> applySettingsStep.error("The settings update could not be applied: {}", settingsUpdateResult.errorMessage()); - case null, default -> applySettingsStep.error("Unexpected value: {}", settingsUpdateResult.updateStatus()); + case SETTINGS_UPDATE_FAILED -> throw new CliFatalException("The settings update could not be applied: " + settingsUpdateResult.errorMessage()); } }); } finally { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateResult.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateResult.java index 27b82b2d4c..87d3fe1e02 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateResult.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateResult.java @@ -2,6 +2,9 @@ import com.devonfw.tools.ide.git.repository.RepositoryType; -public record SettingsUpdateResult(SettingsUpdateStatus updateStatus, RepositoryType repositoryType, String errorMessage) { - -} +/** + * @param updateStatus resulting status of the update operation + * @param repositoryType detected type of the repository + * @param errorMessage error message if updateStatus = {@link SettingsUpdateStatus}.SETTINGS_UPDATE_FAILED, otherwise {@code null} + */ +public record SettingsUpdateResult(SettingsUpdateStatus updateStatus, RepositoryType repositoryType, String errorMessage) {} diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 2eebe1d75d..722afc609b 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -20,7 +20,7 @@ public class RepositoryUtil { */ public static RepositoryType getRepositoryType(Path repositoryPath) { - if (!Files.isDirectory(repositoryPath)) { + if (repositoryPath == null || !Files.isDirectory(repositoryPath)) { return RepositoryType.UNKNOWN; } if (isSettingsFolder(repositoryPath)) { From d9cae42331564c261c5140cff6e0d6763b522e48 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 28 Aug 2026 15:24:38 +0200 Subject: [PATCH 34/89] #1695: - SettingsUpdater now respects force pull settings mode (--force-pull). - SettingsUpdater now correctly saves the commit Id after cloning/pulling. - Improved repositoryType with helper method --- .../update/AbstractUpdateCommandlet.java | 5 +- .../update/settings/SettingsUpdater.java | 111 +++++++++--------- .../ide/git/repository/RepositoryType.java | 9 +- 3 files changed, 65 insertions(+), 60 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 570688b53a..9a483ad272 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -184,7 +184,8 @@ protected String getStepMessage() { private void updateSettingsInStep(Step step) { - SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo); + //TODO: Only check for forcePull flag or also context.forceMode? + SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo, (this.forcePull.isTrue())); try { //Step 1: Perform health check Step healthCheckStep = this.context.newStep("Performing settings health check"); @@ -212,10 +213,10 @@ private void updateSettingsInStep(Step step) { applySettingsStep.run(() -> { SettingsUpdateResult settingsUpdateResult = settingsUpdater.applySettings(healthCheckResult.status() == HealthCheckResultStatus.SETTINGS_VALID_EXISTING, healthCheckResult.temporarySettingsDirectory()); - if (settingsUpdateResult == null) { throw new CliFatalException("Failed to apply the settings update due to unknown error."); } + switch (settingsUpdateResult.updateStatus()) { case SETTINGS_UPDATED -> applySettingsStep.success("Settings update successfully applied"); case SETTINGS_CLONED -> applySettingsStep.success("Settings successfully applied (cloned)"); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index c7ca0d43a8..6a66aa0891 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -3,7 +3,6 @@ import java.nio.file.Files; import java.nio.file.Path; -import org.jline.utils.Log; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,9 +20,9 @@ /** * Handles the settings repository of the current project in two phases: *
    - *
  1. {@link #checkSettings() health check}: the settings are always cloned into a temporary directory first where it is verified that the git URL is valid, + *
  2. {@link #checkSettings(Path)} health check: the settings are always cloned into a temporary directory first where it is verified that the git URL is valid, * that cloning succeeded, and that the repository actually is a settings or a combined code and settings repository.
  3. - *
  4. {@link #applySettings(SettingsHealthCheckResult) apply}: only after the health check succeeded the settings are either pulled in place (if they were already + *
  5. {@link #applySettings(boolean, Path)} apply: only after the health check succeeded the settings are either pulled in place (if they were already * present) or the verified clone is moved to its final location.
  6. *
*/ @@ -52,18 +51,21 @@ public class SettingsUpdater { /** The name of the git project - required to place a combined code and settings repository into the workspace. */ private String gitProjectName; + private boolean isForceMode; + /** * The constructor. * * @param context the {@link IdeContext}. * @param settingsRepoProperty the {@link StringProperty} with the settings repository URL from the update commandlet. */ - public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty) { + public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty, boolean isForceMode) { super(); this.context = context; this.settingsRepoProperty = settingsRepoProperty; this.fileAccess = context.getFileAccess(); + this.isForceMode = isForceMode; } /** @@ -79,7 +81,7 @@ public SettingsHealthCheckResult checkSettings(Path settingsPath) { // for a combined code and settings repository IDE_HOME/settings is a symlink into the code repository whose '.git' folder is one level above, // so isGitRepo would report it as broken settings RepositoryType settingsRepoType = RepositoryUtil.getRepositoryType(settingsPath); - if (isSettingsOrCodeSettingsRepository(settingsRepoType)) { + if (settingsRepoType.isSettingsOrCodeSettingsRepository()) { return checkSettingsPresent(settingsPath, settingsRepoType); } } @@ -87,49 +89,70 @@ public SettingsHealthCheckResult checkSettings(Path settingsPath) { } /** - * Applies the result of the {@link #checkSettings() health check} by either pulling the settings in place or moving the verified clone to its final + * Applies the result of the {@link #checkSettings(Path)} health check by either pulling the settings in place or moving the verified clone to its final * location. * + * @param onlyPull if true, we simply perform a git pull on the actual (not the one in the temp directory) settings repository. * @param sourcePath sourcePath of the settings to apply. * @return a {@link SettingsUpdateResult} representing the state, whether moving/pulling the newest settings was successful. */ - public SettingsUpdateResult applySettings(Path sourcePath) { + public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { RepositoryType repositoryType = RepositoryUtil.getRepositoryType(sourcePath); + Path settingsPath = this.context.getSettingsPath(); + + // Case 1: We performed "ide update"; so settings already existed and we just need to perform a git pull in the existing repo. + if (onlyPull) { + repositoryType = RepositoryUtil.getRepositoryType(context.getSettingsPath()); + if(repositoryType != RepositoryType.SETTINGS) { + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Expected settings repository for update."); + } + pullSettingsAndSaveCommitId(settingsPath); + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATED, repositoryType, null); + } + + // Case 2: We freshly cloned the settings repo and need to move it to a target directory. switch (repositoryType) { - case CODE -> { - //Technically should be caught during a health check, but we still handle this here. + case CODE, UNKNOWN -> { - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, MESSAGE_INVALID_REPOSITORY); + return moveSettingsOnlyIfForceMode(sourcePath, repositoryType); } case SETTINGS -> { //move to IDE_HOME/SETTINGS - moveProject(sourcePath, context.getSettingsPath()); + moveProject(sourcePath, settingsPath); + this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); } case CODE_SETTINGS_COMBINED -> { //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. (Formerly managed by the obsolete "--code" flag) - Path targetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); - moveProject(sourcePath, targetDirectory); - + Path repoMoveTargetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); - Path symlinkTargetPath = this.context.getWorkspacePath().resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS); + Path repoSettingsDirectory = repoMoveTargetDirectory.resolve(IdeContext.FOLDER_SETTINGS); - context.getFileAccess().symlink(symlinkTargetPath, symlinkPath); - this.context.getGitContext().saveCurrentCommitId(symlinkTargetPath, this.context.getSettingsCommitIdPath()); - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); - } - case UNKNOWN -> { + moveProject(sourcePath, repoMoveTargetDirectory); + + context.getFileAccess().symlink(repoSettingsDirectory, symlinkPath); - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, MESSAGE_INVALID_REPOSITORY); + this.context.getGitContext().saveCurrentCommitId(repoSettingsDirectory, this.context.getSettingsCommitIdPath()); + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); } } return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Unknown error during settings"); } + private SettingsUpdateResult moveSettingsOnlyIfForceMode(Path sourcePath, RepositoryType repositoryType) { + if(this.isForceMode) { + moveProject(sourcePath, this.context.getSettingsPath()); + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); + } else { + //Technically should be caught during a health check, but we still handle this here. + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, MESSAGE_INVALID_REPOSITORY); + } + } + /** * Health check for settings that are already present. As the project keeps working with these settings, a failure is only fatal if the user explicitly * aborted. Here, if a new version is available, we clone the new version into a temporary folder and perform health checks. If the cloned, new version is valid, @@ -144,12 +167,12 @@ private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, Reposi cleanup(); //If cloned repo is not (code-)settings repo and no force override (e.g. force mode) is applied, return error. - if (!isSettingsOrCodeSettingsRepository(clonedType) && !requestUserConfirmInvalidRepository(clonedType, gitUrl)) { + if (!clonedType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(clonedType, gitUrl)) { return SettingsHealthCheckResult.failed(clonedType, MESSAGE_INVALID_REPOSITORY, settingsPath); } //Otherwise, (e.g. user overrides), return valid. - return SettingsHealthCheckResult.of(HealthCheckResultStatus.SETTINGS_VALID, repositoryType, settingsPath); + return SettingsHealthCheckResult.of(HealthCheckResultStatus.SETTINGS_VALID_EXISTING, repositoryType, settingsPath); } catch (RuntimeException e) { cleanup(); if (e instanceof CliAbortException) { @@ -171,7 +194,7 @@ private SettingsHealthCheckResult checkClonedSettings(Path settingsPath) { Path tempCloneDir = cloneRepoToTempDir(gitUrl); RepositoryType repositoryType = RepositoryUtil.getRepositoryType(tempCloneDir); - if (!isSettingsOrCodeSettingsRepository(repositoryType) && !requestUserConfirmInvalidRepository(repositoryType, gitUrl)) { + if (!repositoryType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(repositoryType, gitUrl)) { //see @javadoc why we throw fatally here. throw new CliFatalException(MESSAGE_INVALID_REPOSITORY); } @@ -194,13 +217,11 @@ private static CliFatalException createGuaranteedFatalException(RuntimeException } else if (error instanceof CliException) { return new CliFatalException(error.getMessage(), error); } - return new CliFatalException("Failed to set up the settings repository: " + error.getMessage(), error); + return new CliFatalException("Error occurred during settings update: " + error.getClass() + ": " + error.getMessage(), error); } - //TODO: Reimplement this! - private void pullSettings() { + private void pullSettingsAndSaveCommitId(Path settingsPath) { - Path settingsPath = this.context.getSettingsPath(); GitContext gitContext = this.context.getGitContext(); if (gitContext.hasUntrackedFiles(settingsPath)) { gitContext.pullSafelyWithStash(settingsPath); @@ -210,27 +231,6 @@ private void pullSettings() { gitContext.saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); } - private void moveSettings(RepositoryType repositoryType) { - - Path settingsPath = this.context.getSettingsPath(); - if ((repositoryType == RepositoryType.SETTINGS) || (repositoryType == RepositoryType.UNKNOWN)) { - moveProject(this.tempRepoDir, settingsPath); - this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); - } else { - // for a code repository we clone into the workspace and symlink IDE_HOME/settings to its settings folder - Path codePath = this.context.getWorkspacePath().resolve(this.gitProjectName); - moveProject(this.tempRepoDir, codePath); - Path settingsFolder = codePath.resolve(IdeContext.FOLDER_SETTINGS); - if (Files.isDirectory(settingsFolder)) { - this.context.getFileAccess().symlink(settingsFolder, settingsPath); - this.context.getGitContext().saveCurrentCommitId(settingsFolder, this.context.getSettingsCommitIdPath()); - } else { - LOG.warn("The repository has been cloned to {} but it does not contain a settings folder so your project has no settings.", codePath); - } - } - this.tempRepoDir = null; - } - /** * Clone a settings repository into a temporary directory. * @param gitUrl {@link GitUrl} of the (code-)settings repository. @@ -266,18 +266,15 @@ Your settings repository seems to be broken ('.git' folder not present). * @return {@code true} if the user explicitly wants to continue with an invalid repository, {@code false} otherwise. */ private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl) { + LOG.warn("{}\nURL: {}\nDetected repository type: {}", MESSAGE_INVALID_REPOSITORY, gitUrl, repositoryType); - if (!this.context.isForceMode()) { + // If we are in force mode, we give the user the option continue with a potentially invalid repo. If not in FM, we skip asking and act as if he declined. + if(!this.isForceMode) { return false; } - LOG.warn("{}\nURL: {}\nDetected repository type: {}", MESSAGE_INVALID_REPOSITORY, gitUrl, repositoryType); - this.context.askToContinue("Force mode is active. Do you want to continue anyway?"); - return true; - } - - private static boolean isSettingsOrCodeSettingsRepository(RepositoryType repositoryType) { - return (repositoryType == RepositoryType.SETTINGS) || (repositoryType == RepositoryType.CODE_SETTINGS_COMBINED); + this.context.askToContinue("The (update of the) settings repository you are trying to apply seems to be broken. Do you want to continue anyway?"); + return true; } /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java index 81195bc178..2a4a4e5a5e 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java @@ -15,5 +15,12 @@ public enum RepositoryType { CODE_SETTINGS_COMBINED, /** The type of the repository could not be determined. */ - UNKNOWN + UNKNOWN; + + /** + * @return true if repository is either of type {@code SETTINGS} or {@code CODE_SETTINGS_COMBINED} + */ + public boolean isSettingsOrCodeSettingsRepository() { + return this == SETTINGS || this == CODE_SETTINGS_COMBINED; + } } From 796b2756d389aad0b1f6b8c7ec33e22911238b5e Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 28 Aug 2026 17:17:38 +0200 Subject: [PATCH 35/89] #1695: - Small fixes of force mode handling in SettingsUpdater for the case of `ide update` - Changed error strategy in AbstractUpdateCommandlet to use CliException for non-fatal cases instead of step.error() with return - --- .../ide/commandlet/CreateCommandlet.java | 2 +- .../update/AbstractUpdateCommandlet.java | 17 ++++++++++------- .../update/settings/SettingsUpdater.java | 19 ++++++++++++------- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index e5c5b24b82..9bf6014420 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -68,7 +68,7 @@ protected void doRun() { } @Override - protected void onSettingHealthCheckSucceeded() { + protected void onSettingHealthCheckFinished() { // only called after the settings passed the health check Path newProjectPath = getNewProjectPath(); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 9a483ad272..85934fc8c8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -9,13 +9,13 @@ import java.util.Set; import java.util.stream.Stream; +import com.devonfw.tools.ide.cli.CliException; import com.devonfw.tools.ide.cli.CliFatalException; import com.devonfw.tools.ide.commandlet.update.settings.HealthCheckResultStatus; import com.devonfw.tools.ide.commandlet.update.settings.SettingsHealthCheckResult; import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdateResult; import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdater; -import org.jline.utils.Log; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -107,7 +107,7 @@ protected void doRun() { * Hook that is called after the settings passed the health check but before they are moved to their final location. Does nothing by default and is overridden * by {@link CreateCommandlet} to create the project structure so that no project is created at all if the health check failed. */ - protected void onSettingHealthCheckSucceeded() { + protected void onSettingHealthCheckFinished() { // nothing to do by default } @@ -195,22 +195,25 @@ private void updateSettingsInStep(Step step) { HealthCheckResultStatus status = _healthCheckResult.status(); if (status == null) { - throw new CliFatalException("Health check on settings failed due to unknown error - the settings have not been updated"); + throw new CliException("Health check on settings failed due to unknown error - the settings have not been updated"); } else if (status == HealthCheckResultStatus.SETTINGS_INVALID) { - throw new CliFatalException("The settings could not be updated: " + _healthCheckResult.errorMessage()); + throw new CliException("The settings health check failed: " + _healthCheckResult.errorMessage()); } return _healthCheckResult; }, () -> null); - if (healthCheckResult == null) { - throw new CliFatalException("Health check on settings failed due to unknown error - the settings have not been updated"); + + //If health check failed and force mode is disabled, skip application of settings. + if(!this.forcePull.isTrue() && (healthCheckResult == null || healthCheckStep.isFailure())) { + throw new CliException("Settings update aborted due to error in health check"); } //Step 2: Let create/update commandlets prepare themselves for the settings update. - onSettingHealthCheckSucceeded(); + onSettingHealthCheckFinished(); //Step 3: Apply (move/pull newest version) settings Step applySettingsStep = this.context.newStep("Applying settings"); applySettingsStep.run(() -> { + SettingsUpdateResult settingsUpdateResult = settingsUpdater.applySettings(healthCheckResult.status() == HealthCheckResultStatus.SETTINGS_VALID_EXISTING, healthCheckResult.temporarySettingsDirectory()); if (settingsUpdateResult == null) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index 6a66aa0891..b3ecebb36e 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -104,7 +104,7 @@ public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { // Case 1: We performed "ide update"; so settings already existed and we just need to perform a git pull in the existing repo. if (onlyPull) { repositoryType = RepositoryUtil.getRepositoryType(context.getSettingsPath()); - if(repositoryType != RepositoryType.SETTINGS) { + if(repositoryType != RepositoryType.SETTINGS && !this.isForceMode) { return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Expected settings repository for update."); } @@ -116,16 +116,17 @@ public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { switch (repositoryType) { case CODE, UNKNOWN -> { - return moveSettingsOnlyIfForceMode(sourcePath, repositoryType); + return moveSettingsOnlyIfForceModeActive(sourcePath, repositoryType); } case SETTINGS -> { - //move to IDE_HOME/SETTINGS + //move to IDE_HOME/SETTINGS moveProject(sourcePath, settingsPath); this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); } case CODE_SETTINGS_COMBINED -> { + //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. (Formerly managed by the obsolete "--code" flag) Path repoMoveTargetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); @@ -143,7 +144,8 @@ public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Unknown error during settings"); } - private SettingsUpdateResult moveSettingsOnlyIfForceMode(Path sourcePath, RepositoryType repositoryType) { + private SettingsUpdateResult moveSettingsOnlyIfForceModeActive(Path sourcePath, RepositoryType repositoryType) { + LOG.warn("Force mode is active: Moving potentially invalid settings repository to {}", this.context.getSettingsPath()); if(this.isForceMode) { moveProject(sourcePath, this.context.getSettingsPath()); return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); @@ -177,7 +179,7 @@ private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, Reposi cleanup(); if (e instanceof CliAbortException) { // the user answered "no" so we must not silently carry on - throw createGuaranteedFatalException(e); + return SettingsHealthCheckResult.failed(repositoryType, "Settings update aborted by end-user", settingsPath); } return SettingsHealthCheckResult.failed(repositoryType, e.getMessage(), settingsPath); } @@ -194,6 +196,7 @@ private SettingsHealthCheckResult checkClonedSettings(Path settingsPath) { Path tempCloneDir = cloneRepoToTempDir(gitUrl); RepositoryType repositoryType = RepositoryUtil.getRepositoryType(tempCloneDir); + if (!repositoryType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(repositoryType, gitUrl)) { //see @javadoc why we throw fatally here. throw new CliFatalException(MESSAGE_INVALID_REPOSITORY); @@ -266,9 +269,11 @@ Your settings repository seems to be broken ('.git' folder not present). * @return {@code true} if the user explicitly wants to continue with an invalid repository, {@code false} otherwise. */ private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl) { - LOG.warn("{}\nURL: {}\nDetected repository type: {}", MESSAGE_INVALID_REPOSITORY, gitUrl, repositoryType); + LOG.warn("{}\nURL: {}\nDetected settings repository type: {}", MESSAGE_INVALID_REPOSITORY, gitUrl, repositoryType); - // If we are in force mode, we give the user the option continue with a potentially invalid repo. If not in FM, we skip asking and act as if he declined. + /* If we are in force mode, we give the user the option continue with a potentially invalid repo. If not in FM, we skip asking and act as if he declined. + For the case of updating existing repositories, we always want to ask the user regardless of --force-pull + */ if(!this.isForceMode) { return false; } From 79da20217fb213417c81b4caf4a63e7d128c2077 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 28 Aug 2026 17:34:38 +0200 Subject: [PATCH 36/89] #1695: added javadocs --- .../settings/HealthCheckResultStatus.java | 4 +++- .../settings/SettingsHealthCheckResult.java | 13 +++++++----- .../update/settings/SettingsUpdater.java | 20 ++++++++++--------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java index 673ef4f00b..97ae1588e9 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java @@ -1,7 +1,9 @@ package com.devonfw.tools.ide.commandlet.update.settings; +import java.nio.file.Path; + /** - * Status of the settings {@link SettingsUpdater#checkSettings() health check} describing what {@link SettingsUpdater#applySettings(SettingsHealthCheckResult)} has + * Status of the settings {@link SettingsUpdater#checkSettings(Path)} health check} describing what {@link SettingsUpdater#applySettings(boolean, Path)} has * to do. */ public enum HealthCheckResultStatus { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java index e28692c112..e644a03f56 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java @@ -5,31 +5,34 @@ import java.nio.file.Path; /** - * Result of the settings {@link SettingsUpdater#checkSettings() health check}. + * Result of the settings {@link SettingsUpdater#checkSettings(Path)} health check}. * * @param status the {@link HealthCheckResultStatus}. * @param repositoryType the {@link RepositoryType} of the settings repository. * @param errorMessage the reason why the settings could not be updated or {@code null} if the health check succeeded. + * @param temporarySettingsDirectory path to the temporary folder this health check was performed on. */ public record SettingsHealthCheckResult(HealthCheckResultStatus status, RepositoryType repositoryType, Path temporarySettingsDirectory, String errorMessage) { /** * @param status the {@link HealthCheckResultStatus}. * @param repositoryType the {@link RepositoryType}. + * @param temporarySettingsDirectory path to the temporary folder this health check was performed on. * @return a {@link SettingsHealthCheckResult} for a successful health check. */ - public static SettingsHealthCheckResult of(HealthCheckResultStatus status, RepositoryType repositoryType, Path temporaryRepoDirectory) { + public static SettingsHealthCheckResult of(HealthCheckResultStatus status, RepositoryType repositoryType, Path temporarySettingsDirectory) { - return new SettingsHealthCheckResult(status, repositoryType, temporaryRepoDirectory, null); + return new SettingsHealthCheckResult(status, repositoryType, temporarySettingsDirectory, null); } /** * @param repositoryType the {@link RepositoryType} of the settings that are already present. * @param errorMessage the reason why the settings could not be updated. + * @param temporarySettingsDirectory path to the temporary folder this health check was performed on. * @return a {@link SettingsHealthCheckResult} for a failed but recoverable health check. */ - public static SettingsHealthCheckResult failed(RepositoryType repositoryType, String errorMessage, Path temporaryRepoDirectory) { + public static SettingsHealthCheckResult failed(RepositoryType repositoryType, String errorMessage, Path temporarySettingsDirectory) { - return new SettingsHealthCheckResult(HealthCheckResultStatus.SETTINGS_INVALID, repositoryType, temporaryRepoDirectory, errorMessage); + return new SettingsHealthCheckResult(HealthCheckResultStatus.SETTINGS_INVALID, repositoryType, temporarySettingsDirectory, errorMessage); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index b3ecebb36e..abad1a29dc 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -51,13 +51,14 @@ public class SettingsUpdater { /** The name of the git project - required to place a combined code and settings repository into the workspace. */ private String gitProjectName; - private boolean isForceMode; + private final boolean isForceMode; /** * The constructor. * * @param context the {@link IdeContext}. * @param settingsRepoProperty the {@link StringProperty} with the settings repository URL from the update commandlet. + * @param isForceMode if in force mode, the settings health check will always return either {@link HealthCheckResultStatus#SETTINGS_VALID} or {@link HealthCheckResultStatus#SETTINGS_VALID_EXISTING} */ public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty, boolean isForceMode) { @@ -73,6 +74,7 @@ public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty, * is backed up. Whether the settings are pulled or cloned is decided solely by the state of {@link IdeContext#getSettingsPath() IDE_HOME/settings} so that * {@code ide create} and {@code ide update} share the very same logic. * + * @param settingsPath the path to the (code-)settings directory which the health check should be performed on. * @return the {@link SettingsHealthCheckResult}. */ public SettingsHealthCheckResult checkSettings(Path settingsPath) { @@ -169,7 +171,7 @@ private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, Reposi cleanup(); //If cloned repo is not (code-)settings repo and no force override (e.g. force mode) is applied, return error. - if (!clonedType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(clonedType, gitUrl)) { + if (!clonedType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(clonedType, gitUrl, true)) { return SettingsHealthCheckResult.failed(clonedType, MESSAGE_INVALID_REPOSITORY, settingsPath); } @@ -197,7 +199,7 @@ private SettingsHealthCheckResult checkClonedSettings(Path settingsPath) { Path tempCloneDir = cloneRepoToTempDir(gitUrl); RepositoryType repositoryType = RepositoryUtil.getRepositoryType(tempCloneDir); - if (!repositoryType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(repositoryType, gitUrl)) { + if (!repositoryType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(repositoryType, gitUrl, false)) { //see @javadoc why we throw fatally here. throw new CliFatalException(MESSAGE_INVALID_REPOSITORY); } @@ -268,17 +270,17 @@ Your settings repository seems to be broken ('.git' folder not present). /** * @return {@code true} if the user explicitly wants to continue with an invalid repository, {@code false} otherwise. */ - private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl) { - LOG.warn("{}\nURL: {}\nDetected settings repository type: {}", MESSAGE_INVALID_REPOSITORY, gitUrl, repositoryType); - + private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl, boolean updatesExistingRepository) { /* If we are in force mode, we give the user the option continue with a potentially invalid repo. If not in FM, we skip asking and act as if he declined. - For the case of updating existing repositories, we always want to ask the user regardless of --force-pull + For the case of updating existing settings repositories, we always want to ask the user regardless of --force-pull, as this could break the setup. */ - if(!this.isForceMode) { + if(!this.isForceMode && !updatesExistingRepository) { return false; } - this.context.askToContinue("The (update of the) settings repository you are trying to apply seems to be broken. Do you want to continue anyway?"); + LOG.warn("{}\nURL: {}\nDetected settings repository type: {}", MESSAGE_INVALID_REPOSITORY, gitUrl, repositoryType); + + this.context.askToContinue("The update to the settings repository you are trying to apply seems to be broken. Do you want to continue anyway?"); return true; } From 9dd9cb63d3dc6ccbad687ff841220c47e4dd52c5 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 28 Aug 2026 17:39:23 +0200 Subject: [PATCH 37/89] #1695: resolved checkstyle violations --- .../update/settings/SettingsUpdater.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index abad1a29dc..92706d8a8f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -20,8 +20,9 @@ /** * Handles the settings repository of the current project in two phases: *
    - *
  1. {@link #checkSettings(Path)} health check: the settings are always cloned into a temporary directory first where it is verified that the git URL is valid, - * that cloning succeeded, and that the repository actually is a settings or a combined code and settings repository.
  2. + *
  3. {@link #checkSettings(Path)} health check: the settings are always cloned into a temporary directory + * first where it is verified that the git URL is valid, that cloning succeeded, + * and that the repository actually is a settings or a combined code and settings repository.
  4. *
  5. {@link #applySettings(boolean, Path)} apply: only after the health check succeeded the settings are either pulled in place (if they were already * present) or the verified clone is moved to its final location.
  6. *
@@ -58,7 +59,8 @@ public class SettingsUpdater { * * @param context the {@link IdeContext}. * @param settingsRepoProperty the {@link StringProperty} with the settings repository URL from the update commandlet. - * @param isForceMode if in force mode, the settings health check will always return either {@link HealthCheckResultStatus#SETTINGS_VALID} or {@link HealthCheckResultStatus#SETTINGS_VALID_EXISTING} + * @param isForceMode if in force mode, the settings health check will always return either {@link HealthCheckResultStatus#SETTINGS_VALID} or + * {@link HealthCheckResultStatus#SETTINGS_VALID_EXISTING} */ public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty, boolean isForceMode) { @@ -129,7 +131,8 @@ public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { } case CODE_SETTINGS_COMBINED -> { - //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. (Formerly managed by the obsolete "--code" flag) + //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. + // (Formerly managed by the obsolete "--code" flag) Path repoMoveTargetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); Path repoSettingsDirectory = repoMoveTargetDirectory.resolve(IdeContext.FOLDER_SETTINGS); @@ -159,8 +162,8 @@ private SettingsUpdateResult moveSettingsOnlyIfForceModeActive(Path sourcePath, /** * Health check for settings that are already present. As the project keeps working with these settings, a failure is only fatal if the user explicitly - * aborted. Here, if a new version is available, we clone the new version into a temporary folder and perform health checks. If the cloned, new version is valid, - * we call git update in the existing settings folder. + * aborted. Here, if a new version is available, we clone the new version into a temporary folder and perform health checks. + * If the cloned, new version is valid, we call git update in the existing settings folder. */ private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, RepositoryType repositoryType) { From ad28a7a70cd84678bac4c16b0a9c54b37334a1ea Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 28 Aug 2026 18:57:33 +0200 Subject: [PATCH 38/89] #1695: - fixed SettingsUpdater not recognizing context.isForceMode() - Fixed "Update settings" step not failing if "Apply update" step fails - RepositoryUtil now also checks for the presence of a .git folder - Fixed some altered log messages in UpdateCommandletTest --- .../commandlet/update/AbstractUpdateCommandlet.java | 7 ++++++- .../commandlet/update/settings/SettingsUpdater.java | 1 - .../tools/ide/git/repository/RepositoryUtil.java | 10 +++++++--- .../tools/ide/commandlet/UpdateCommandletTest.java | 5 ++--- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 85934fc8c8..1ca28f7904 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -185,7 +185,7 @@ protected String getStepMessage() { private void updateSettingsInStep(Step step) { //TODO: Only check for forcePull flag or also context.forceMode? - SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo, (this.forcePull.isTrue())); + SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo, (this.forcePull.isTrue() || this.context.isForceMode())); try { //Step 1: Perform health check Step healthCheckStep = this.context.newStep("Performing settings health check"); @@ -226,6 +226,11 @@ private void updateSettingsInStep(Step step) { case SETTINGS_UPDATE_FAILED -> throw new CliFatalException("The settings update could not be applied: " + settingsUpdateResult.errorMessage()); } }); + + //Make sure to always fail the parent step if the "Apply settings" step fails. + if(applySettingsStep.isFailure()) { + throw new CliException("Settings update failed due to error while applying the settings update"); + } } finally { // the verified clone lives across both steps and the prepareProject hook so it is only here that its lifetime ends settingsUpdater.cleanup(); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index 92706d8a8f..966d1df9bd 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -316,7 +316,6 @@ private GitUrl getOrAskSettingsUrl() { } String userPrompt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; while ((gitUrl == null) || !gitUrl.isValid()) { - LOG.warn("The provided git url parameter {} was detected to be invalid. Please enter a valid settings url.", gitUrl); repository = handleDefaultRepository(this.context.askForInput(userPrompt, IdeContext.DEFAULT_SETTINGS_REPO_URL)); gitUrl = GitUrl.of(repository); if (!gitUrl.isValid()) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 722afc609b..044148477f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -5,6 +5,9 @@ import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.environment.EnvironmentVariables; +import com.devonfw.tools.ide.git.GitContext; +import com.devonfw.tools.ide.git.GitContextImpl; +import com.devonfw.tools.ide.git.GitUrl; /** * Utility class for IDEasy settings/code repositories. @@ -33,7 +36,7 @@ public static RepositoryType getRepositoryType(Path repositoryPath) { if (!Files.exists(settingsFolder)) { return RepositoryType.CODE; } - // there is a settings folder but it does not contain the required properties file + // there is no valid settings folder to be found. return RepositoryType.UNKNOWN; } @@ -43,7 +46,8 @@ public static RepositoryType getRepositoryType(Path repositoryPath) { */ private static boolean isSettingsFolder(Path folder) { - return Files.exists(folder.resolve(EnvironmentVariables.DEFAULT_PROPERTIES)) - || Files.exists(folder.resolve(EnvironmentVariables.LEGACY_PROPERTIES)); + return Files.exists(folder.resolve(GitContext.GIT_FOLDER)) && + (Files.exists(folder.resolve(EnvironmentVariables.DEFAULT_PROPERTIES)) + || Files.exists(folder.resolve(EnvironmentVariables.LEGACY_PROPERTIES))); } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java index 8715b74b4e..a1efb8035a 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java @@ -28,7 +28,7 @@ class UpdateCommandletTest extends AbstractIdeContextTest { private static final String PROJECT_UPDATE = "update"; - private static final String SUCCESS_UPDATE_SETTINGS = "Successfully ended step 'update (pull) settings repository'."; + private static final String SUCCESS_UPDATE_SETTINGS = "Successfully ended step 'Update settings repository'."; private static final String SUCCESS_INSTALL_OR_UPDATE_SOFTWARE = "Install or update software"; @Test @@ -179,7 +179,6 @@ void testRunUpdateWithBrokenSettingsFolder() { // assert assertThat(context).logAtSuccess().hasMessage(SUCCESS_UPDATE_SETTINGS); - assertThat(context).logAtInfo().hasMessageContaining("Creating backup by moving " + settingsPath); assertThat(context.getIdeHome().resolve(IdeContext.FOLDER_BACKUPS)).exists(); assertThat(settingsPath.resolve(GitContext.GIT_FOLDER)).exists(); assertThat(context).logAtSuccess().hasMessageContaining(SUCCESS_INSTALL_OR_UPDATE_SOFTWARE); @@ -208,7 +207,7 @@ public void pull(Path repository) { update.run(); // assert - assertThat(context).logAtError().hasMessage("Step 'Applying update' ended with failure."); + assertThat(context).logAtError().hasMessage("Step 'Applying settings' ended with failure."); assertThat(context).log().hasNoMessage(SUCCESS_UPDATE_SETTINGS); assertThat(context).logAtSuccess().hasMessageContaining(SUCCESS_INSTALL_OR_UPDATE_SOFTWARE); } From 6da3b3a3e5478f0efc6a03498e6893630f30f65a Mon Sep 17 00:00:00 2001 From: laim2003 Date: Thu, 13 Aug 2026 16:10:14 +0200 Subject: [PATCH 39/89] #1695: initial commit Signed-off-by: laim2003 --- .../commandlet/AbstractUpdateCommandlet.java | 65 ++++++++++++++++++- .../ide/git/repository/RepositoryUtil.java | 53 +++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java index c034ddcf24..da7ac34327 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java @@ -19,6 +19,7 @@ import com.devonfw.tools.ide.git.GitContext; import com.devonfw.tools.ide.git.GitUrl; import com.devonfw.tools.ide.git.repository.RepositoryCommandlet; +import com.devonfw.tools.ide.git.repository.RepositoryUtil; import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.property.FlagProperty; import com.devonfw.tools.ide.property.StringProperty; @@ -182,6 +183,7 @@ private void updateSettingsInStep(boolean codeRepository) { if (!settingsRepository) { if (Files.exists(settingsPath)) { if (!this.context.getFileAccess().isEmptyDir(settingsPath)) { + // settings folder seems to be invalid this.context.askToContinue( "Your settings repository seems to be broken ('.git' folder not present). " + "We can fix this by moving your settings the backed up. " @@ -191,9 +193,10 @@ private void updateSettingsInStep(boolean codeRepository) { } this.context.getFileAccess().backup(settingsPath); } + //settings folder does not exist (yet), lets retrieve the settings url to pull GitUrl gitUrl = getOrAskSettingsUrl(); checkProjectNameConvention(gitUrl.getProjectName()); - initializeRepository(gitUrl); + pullAndVerify(gitUrl); return; } } @@ -236,6 +239,64 @@ private GitUrl getOrAskSettingsUrl() { return gitUrl; } + /** + * We pull the settings repo from the remote into a temporary folder to perform health checks. + */ + private void pullAndVerify(GitUrl gitUrl) { + GitContext gitContext = this.context.getGitContext(); + Path tempProjectPath = this.context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS).resolve(this.context.getProjectName()); + + gitContext.pullOrClone(gitUrl, tempProjectPath); + + checkIntegrityAndMove(tempProjectPath, gitUrl.getProjectName()); + } + + private void checkIntegrityAndMove(Path projectPath, String gitProjectName) { + + FileAccess fileAccess = this.context.getFileAccess(); + + if (!Files.exists(projectPath)) { + throw new CliException(getIntegrityCheckErrorMessage("Git pull target folder does not exist.")); + } + + Path finalSettingsPath; + switch (RepositoryUtil.getRepositoryType(projectPath, gitProjectName)) { + case CODE -> { + + finalSettingsPath = this.context.getIdeHome().resolve(this.context.getProjectName()).resolve(IdeContext.FOLDER_SETTINGS); + moveProject(projectPath, finalSettingsPath); + } + case SETTINGS -> { + + finalSettingsPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); + moveProject(projectPath, finalSettingsPath); + } + case CODE_SETTINGS_COMBINED -> { + + finalSettingsPath = this.context.getWorkspacePath().resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS); + Path symLinkLocation = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); + + } + case UNKNOWN -> + throw new CliException(getIntegrityCheckErrorMessage("The specified repository could not be validated as either a code or settings repository.")); + } + } + + private Path moveProject(Path from, Path to) { + + FileAccess fileAccess = this.context.getFileAccess(); + try { + fileAccess.move(from, to); + } catch (Exception e) { + throw new CliException(getIntegrityCheckErrorMessage(String.format("Failed to move project from %s to %s", from, to)), e); + } + return to; + } + + private String getIntegrityCheckErrorMessage(String message) { + return String.format("Settings repository integrity check failed: %s", message); + } + private String handleDefaultRepository(String repository) { if ("-".equals(repository)) { if (isCodeRepository()) { @@ -275,7 +336,7 @@ private void initializeRepository(GitUrl gitUrl) { Path settingsPath = this.context.getSettingsPath(); Path repoPath = settingsPath; boolean codeRepository = isCodeRepository(); - if (codeRepository) { + if (codeRepository) { //this never gets executed because isCodeRepository is always false // clone the given code repository into IDE_HOME/workspaces/main repoPath = context.getWorkspacePath().resolve(gitUrl.getProjectName()); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java new file mode 100644 index 0000000000..66db4d1b4a --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -0,0 +1,53 @@ +package com.devonfw.tools.ide.git.repository; + +import java.nio.file.Files; +import java.nio.file.Path; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.environment.EnvironmentVariables; + +/// Utility class for IDEasy settings/code repositories +public class RepositoryUtil { + + /** + * Checks whether te given repository is a settings repository, a combined settings and code repository, or a typical code repositor. Combined code and + * settings repository is detected by checking whether IDE_HOME/workspaces/main/[gitProjectName]/settings exists and is a valid settings repository. + * + * @param repositoryPath - The path of the repository to be checked. + * @return {@link RepositoryType} of the repository. + */ + public static RepositoryType getRepositoryType(Path repositoryPath, String gitProjectName) { + + if (!Files.exists(repositoryPath)) { + return RepositoryType.UNKNOWN; + } + + if (Files.exists(repositoryPath.resolve(EnvironmentVariables.DEFAULT_PROPERTIES)) + || Files.exists(repositoryPath.resolve(EnvironmentVariables.LEGACY_PROPERTIES))) { + return RepositoryType.SETTINGS; + } else if (gitProjectName != null + && Files.exists( + repositoryPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN).resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS)) + && getRepositoryType( + repositoryPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN).resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS), + gitProjectName) == RepositoryType.SETTINGS) { + return RepositoryType.CODE_SETTINGS_COMBINED; + } else if (!Files.isSymbolicLink(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS)) + && getRepositoryType(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), gitProjectName) == RepositoryType.SETTINGS) { + return RepositoryType.CODE; + } + return RepositoryType.UNKNOWN; + } + + /// enum representation of a detected {@link RepositoryType} + public enum RepositoryType { + /// Git Repository is a code repository. + CODE, + /// Git Repository is a settings repository. + SETTINGS, + /// A combined code & settings repository contains both the settings-folder and the code within the workspace folder. + CODE_SETTINGS_COMBINED, + /// The type of the repository could not be determined. + UNKNOWN + } +} From cecb1c3807b588d88e479bbe2fce44b9e65ef68f Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 15:29:55 +0200 Subject: [PATCH 40/89] #1695: removed code repository flag Signed-off-by: laim2003 --- .../tools/ide/commandlet/CreateCommandlet.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index a68d6768c5..ed2e937404 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -10,7 +10,6 @@ import com.devonfw.tools.ide.context.IdeContext; 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; @@ -24,9 +23,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. * @@ -36,7 +32,6 @@ public CreateCommandlet(IdeContext context) { super(context); this.newProject = add(new StringProperty("", true, "project")); - this.codeRepositoryFlag = add(new FlagProperty("--code")); add(this.settingsRepo); } @@ -82,15 +77,10 @@ private void initializeProject(Path newInstancePath) { fileAccess.mkdirs(newInstancePath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)); } - @Override - protected boolean isCodeRepository() { - return this.codeRepositoryFlag.isTrue(); - } - @Override protected String getStepMessage() { - return "Create (clone) " + (isCodeRepository() ? "code" : "settings") + " repository"; + return "Create (clone) repository"; } private void logWelcomeMessage() { From dd8e2174d58278c54efb164176b88b395096375f Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 15:30:30 +0200 Subject: [PATCH 41/89] #1695: Added new helper class to determine the type of a repository. Signed-off-by: laim2003 --- .../ide/git/repository/RepositoryUtil.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 66db4d1b4a..413f45552d 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -10,7 +10,7 @@ public class RepositoryUtil { /** - * Checks whether te given repository is a settings repository, a combined settings and code repository, or a typical code repositor. Combined code and + * Checks whether te given git repository is a settings repository, a combined settings and code repository, or a typical code repositor. Combined code and * settings repository is detected by checking whether IDE_HOME/workspaces/main/[gitProjectName]/settings exists and is a valid settings repository. * * @param repositoryPath - The path of the repository to be checked. @@ -27,13 +27,12 @@ public static RepositoryType getRepositoryType(Path repositoryPath, String gitPr return RepositoryType.SETTINGS; } else if (gitProjectName != null && Files.exists( - repositoryPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN).resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS)) + repositoryPath.resolve(IdeContext.FOLDER_SETTINGS)) && getRepositoryType( - repositoryPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN).resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS), + repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), gitProjectName) == RepositoryType.SETTINGS) { return RepositoryType.CODE_SETTINGS_COMBINED; - } else if (!Files.isSymbolicLink(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS)) - && getRepositoryType(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), gitProjectName) == RepositoryType.SETTINGS) { + } else if (!Files.exists(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS))) { return RepositoryType.CODE; } return RepositoryType.UNKNOWN; @@ -42,12 +41,15 @@ && getRepositoryType(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), gitProj /// enum representation of a detected {@link RepositoryType} public enum RepositoryType { /// Git Repository is a code repository. - CODE, + CODE("code"), /// Git Repository is a settings repository. - SETTINGS, + SETTINGS("settings"), /// A combined code & settings repository contains both the settings-folder and the code within the workspace folder. - CODE_SETTINGS_COMBINED, + CODE_SETTINGS_COMBINED("code & settings"), /// The type of the repository could not be determined. - UNKNOWN + UNKNOWN("unknown"); + + RepositoryType(String displayName) { + } } } From 48121531af5f47e454cf5fc9f1266e2955b868e8 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 16:12:58 +0200 Subject: [PATCH 42/89] #1695: added logic that moves a settings directory to a temp dir, verifies its health and then moves it to the IDE_HOME Signed-off-by: laim2003 --- .../commandlet/AbstractUpdateCommandlet.java | 111 ++++-------------- 1 file changed, 25 insertions(+), 86 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java index da7ac34327..397d8b6157 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java @@ -195,8 +195,7 @@ private void updateSettingsInStep(boolean codeRepository) { } //settings folder does not exist (yet), lets retrieve the settings url to pull GitUrl gitUrl = getOrAskSettingsUrl(); - checkProjectNameConvention(gitUrl.getProjectName()); - pullAndVerify(gitUrl); + pullAndCheckIntegrity(gitUrl); return; } } @@ -213,17 +212,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 = "Settings 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); @@ -242,7 +234,7 @@ private GitUrl getOrAskSettingsUrl() { /** * We pull the settings repo from the remote into a temporary folder to perform health checks. */ - private void pullAndVerify(GitUrl gitUrl) { + private void pullAndCheckIntegrity(GitUrl gitUrl) { GitContext gitContext = this.context.getGitContext(); Path tempProjectPath = this.context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS).resolve(this.context.getProjectName()); @@ -259,26 +251,31 @@ private void checkIntegrityAndMove(Path projectPath, String gitProjectName) { throw new CliException(getIntegrityCheckErrorMessage("Git pull target folder does not exist.")); } - Path finalSettingsPath; + Path targetDirectory; switch (RepositoryUtil.getRepositoryType(projectPath, gitProjectName)) { - case CODE -> { - - finalSettingsPath = this.context.getIdeHome().resolve(this.context.getProjectName()).resolve(IdeContext.FOLDER_SETTINGS); - moveProject(projectPath, finalSettingsPath); - } + case CODE -> throw new CliException( + getIntegrityCheckErrorMessage( + "The given git repository URL points to a code repository. The <> parameter only accepts a settings or a combined code-settings repository.")); case SETTINGS -> { - finalSettingsPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); - moveProject(projectPath, finalSettingsPath); + targetDirectory = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); + moveProject(projectPath, targetDirectory); + this.context.getGitContext().saveCurrentCommitId(targetDirectory, this.context.getSettingsCommitIdPath()); } case CODE_SETTINGS_COMBINED -> { - finalSettingsPath = this.context.getWorkspacePath().resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS); - Path symLinkLocation = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); + //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. (Formerly managed by the obsolete "--code" flag) + targetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); + moveProject(projectPath, targetDirectory); + + Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); + Path symlinkTargetPath = this.context.getWorkspacePath().resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS); + fileAccess.symlink(symlinkTargetPath, symlinkPath); + this.context.getGitContext().saveCurrentCommitId(symlinkTargetPath, this.context.getSettingsCommitIdPath()); } - case UNKNOWN -> - throw new CliException(getIntegrityCheckErrorMessage("The specified repository could not be validated as either a code or settings repository.")); + case UNKNOWN -> throw new CliException( + getIntegrityCheckErrorMessage("The specified repository could not be validated as either a code, settings or combined code-settings repository.")); } } @@ -299,60 +296,12 @@ private String getIntegrityCheckErrorMessage(String message) { 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 settings 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) { //this never gets executed because isCodeRepository is always false - // 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()); - } - private void updateSoftware() { if (this.skipTools.isTrue()) { @@ -508,14 +457,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; - } - } From c145c223b7f6194ba35dc59693e64d161c09d467 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 16:14:04 +0200 Subject: [PATCH 43/89] #1695: fixed typo Signed-off-by: laim2003 --- .../tools/ide/commandlet/AbstractUpdateCommandlet.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java index 397d8b6157..188017b495 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java @@ -212,7 +212,7 @@ private GitUrl getOrAskSettingsUrl() { String repository = this.settingsRepo.getValue(); repository = handleDefaultRepository(repository); - String userPromt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; + String userPrompt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; String defaultUrl = IdeContext.DEFAULT_SETTINGS_REPO_URL; LOG.info(MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath()); @@ -221,7 +221,7 @@ private GitUrl getOrAskSettingsUrl() { gitUrl = GitUrl.of(repository); } while ((gitUrl == null) || !gitUrl.isValid()) { - repository = this.context.askForInput(userPromt, defaultUrl); + repository = this.context.askForInput(userPrompt, defaultUrl); repository = handleDefaultRepository(repository); gitUrl = GitUrl.of(repository); if (!gitUrl.isValid()) { From 3b0e1d29e84a3ba68f73691d4896d8c977e8b306 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 16:21:44 +0200 Subject: [PATCH 44/89] #1695: cleanup of RepositoryUtil Signed-off-by: laim2003 --- .../com/devonfw/tools/ide/git/repository/RepositoryUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 413f45552d..1b940397e8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -45,7 +45,7 @@ public enum RepositoryType { /// Git Repository is a settings repository. SETTINGS("settings"), /// A combined code & settings repository contains both the settings-folder and the code within the workspace folder. - CODE_SETTINGS_COMBINED("code & settings"), + CODE_SETTINGS_COMBINED("code & settings combined"), /// The type of the repository could not be determined. UNKNOWN("unknown"); From c2b3830d60d92132a0ba80d7d8750ebd2bb4d7b9 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 16:28:52 +0200 Subject: [PATCH 45/89] #1695: updated CHANGELOG.adoc Signed-off-by: laim2003 --- CHANGELOG.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index c7377d8efd..6633aebe56 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -5,7 +5,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE == 2026.09.002 Release with new features and bugfixes: - +* https://github.com/devonfw/IDEasy/issues/1695[#1695]: Project creation logic extended with health checks and removed `--code` flag. The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/50?closed=1[milestone 2026.09.002]. From 429f05a00fdb104f1a4772c4b19856008f08305b Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 16:29:13 +0200 Subject: [PATCH 46/89] #1695: cleanup of RepositoryUtil Signed-off-by: laim2003 --- .../tools/ide/git/repository/RepositoryUtil.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 1b940397e8..9a6be63c13 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -41,15 +41,12 @@ && getRepositoryType( /// enum representation of a detected {@link RepositoryType} public enum RepositoryType { /// Git Repository is a code repository. - CODE("code"), + CODE, /// Git Repository is a settings repository. - SETTINGS("settings"), + SETTINGS, /// A combined code & settings repository contains both the settings-folder and the code within the workspace folder. - CODE_SETTINGS_COMBINED("code & settings combined"), + CODE_SETTINGS_COMBINED, /// The type of the repository could not be determined. - UNKNOWN("unknown"); - - RepositoryType(String displayName) { - } + UNKNOWN } } From 5d13b4a5757e6d1dad19f47f620d5016894b9206 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 16:43:12 +0200 Subject: [PATCH 47/89] #1695: removed help description for --code flag Signed-off-by: laim2003 --- cli/src/main/resources/nls/Help.properties | 1 - cli/src/main/resources/nls/Help_de.properties | 1 - 2 files changed, 2 deletions(-) diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties index e678805cc2..0f9d3dabbe 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -187,7 +187,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 diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties index 8276ff9bae..3413367b59 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -187,7 +187,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. From d615252c92322a4611f6a414985ab749e19c6283 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 14 Aug 2026 16:47:23 +0200 Subject: [PATCH 48/89] #1695: Updated documentation Signed-off-by: laim2003 --- documentation/settings.adoc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/documentation/settings.adoc b/documentation/settings.adoc index e9de2335ca..3d7a1540a1 100644 --- a/documentation/settings.adoc +++ b/documentation/settings.adoc @@ -18,17 +18,18 @@ This gives you the freedom to control and manage the tools with their versions a To setup and customize these settings simply follow the link:usage.adoc#admin[admin usage guide]. Then tell your team to create the project using your project sepcific settings git URL: ``` -ide create «project-name» --code «settings-url» +ide create «project-name» «settings-url» ``` == Code-repository It is even possible to include your settings into your code repository by having the `settings` folder directly on top-level of your code git repository. This allows you to keep settings changes in sync with code changes and manage them in the same pull/merge requests. -To use this approach simply copy the content of https://github.com/devonfw/ide-settings[ide-settings] to a top-level `settings` folder in your code repository root and tell your developers to create the project usining the `--code` option: +To use this approach simply copy the content of https://github.com/devonfw/ide-settings[ide-settings] to a top-level `settings` folder in your code repository root. +IDEasy will automatically recognize that you are using a code repository, therefore just use the same command as above: ``` -ide create «project-name» --code «code-repo-url» +ide create «project-name» «code-repo-url» ``` IDEasy will clone your repository and create a symlink to the settings folder. From e7d975b306a872fd60b4e70e7d1d5c12f35ac31d Mon Sep 17 00:00:00 2001 From: laim2003 Date: Mon, 17 Aug 2026 14:46:13 +0200 Subject: [PATCH 49/89] #1695: small fixes Signed-off-by: laim2003 --- .../commandlet/AbstractUpdateCommandlet.java | 22 +++++++++---------- .../devonfw/tools/ide/git/GitContextMock.java | 3 +++ .../test/resources/settings/ide.properties | 0 3 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 cli/src/test/resources/settings/ide.properties diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java index 188017b495..e6b049825f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java @@ -168,7 +168,7 @@ protected void updateSettings() { LOG.info("Skipping git pull in settings due to code repository. Use --force-pull to enforce pulling."); return; } - this.context.newStep(getStepMessage()).run(() -> updateSettingsInStep(codeRepository)); + this.context.newStep(getStepMessage()).run(() -> updateSettingsInStep(codeRepository), true); } protected String getStepMessage() { @@ -185,11 +185,8 @@ private void updateSettingsInStep(boolean codeRepository) { if (!this.context.getFileAccess().isEmptyDir(settingsPath)) { // settings folder seems to be invalid this.context.askToContinue( - "Your settings repository seems to be broken ('.git' folder not present). " - + "We can fix this by moving your settings the backed up. " - + "You will be asked for the settings git URL and your settings will be cloned from scratch. " - + "Do you want to proceed?" - ); + "Your settings repository seems to be broken ('.git' folder not present). " + "We can fix this by moving your settings the backed up. " + + "You will be asked for the settings git URL and your settings will be cloned from scratch. " + "Do you want to proceed?"); } this.context.getFileAccess().backup(settingsPath); } @@ -253,9 +250,6 @@ private void checkIntegrityAndMove(Path projectPath, String gitProjectName) { Path targetDirectory; switch (RepositoryUtil.getRepositoryType(projectPath, gitProjectName)) { - case CODE -> throw new CliException( - getIntegrityCheckErrorMessage( - "The given git repository URL points to a code repository. The <> parameter only accepts a settings or a combined code-settings repository.")); case SETTINGS -> { targetDirectory = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); @@ -274,8 +268,12 @@ private void checkIntegrityAndMove(Path projectPath, String gitProjectName) { fileAccess.symlink(symlinkTargetPath, symlinkPath); this.context.getGitContext().saveCurrentCommitId(symlinkTargetPath, this.context.getSettingsCommitIdPath()); } - case UNKNOWN -> throw new CliException( - getIntegrityCheckErrorMessage("The specified repository could not be validated as either a code, settings or combined code-settings repository.")); + default -> { + fileAccess.backup(projectPath); + throw new CliException(getIntegrityCheckErrorMessage(String.format( + "The given git repository URL does not point to a valid settings or code-settings repository. Please verify and try again. Before trying again, please delete the folder %s", + this.context.getIdeHome()))); + } } } @@ -296,7 +294,7 @@ private String getIntegrityCheckErrorMessage(String message) { private String handleDefaultRepository(String repository) { if ("-".equals(repository)) { - LOG.info("'-' was found for settings repository, the default settings repository '{}' will be used.", 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; diff --git a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java index d794f0ad12..10a5e3f023 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java @@ -57,6 +57,9 @@ public void clone(GitUrl gitUrl, Path repository) { FileAccess fileAccess = this.context.getFileAccess(); fileAccess.mkdirs(repository); + // Create ide.properties to simulate a valid repository + fileAccess.touch(repository.resolve("ide.properties")); + Path gitFolder = repository.resolve(GIT_FOLDER); fileAccess.mkdirs(gitFolder); String branch = gitUrl.branch(); diff --git a/cli/src/test/resources/settings/ide.properties b/cli/src/test/resources/settings/ide.properties new file mode 100644 index 0000000000..e69de29bb2 From c9306605c64600772788db0ac239ef216560396a Mon Sep 17 00:00:00 2001 From: laim2003 Date: Mon, 17 Aug 2026 14:46:22 +0200 Subject: [PATCH 50/89] #1695: added tests Signed-off-by: laim2003 --- .../ide/commandlet/CreateCommandletTest.java | 76 ++++++------------- 1 file changed, 24 insertions(+), 52 deletions(-) diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java index 98534e72a9..06b3b5ca4c 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java @@ -7,15 +7,12 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import com.devonfw.tools.ide.cli.CliArguments; import com.devonfw.tools.ide.cli.CliException; import com.devonfw.tools.ide.context.AbstractIdeContextTest; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.context.IdeTestContext; -import com.devonfw.tools.ide.context.ProcessContextGitMock; import com.devonfw.tools.ide.environment.EnvironmentVariables; import com.devonfw.tools.ide.environment.EnvironmentVariablesType; import com.devonfw.tools.ide.git.GitContextImplMock; @@ -67,54 +64,7 @@ void testCreateCommandletRun() { assertThat(newProjectPath.resolve(IdeContext.FOLDER_PLUGINS)).exists(); assertThat(newProjectPath.resolve(IdeContext.FOLDER_SOFTWARE)).exists(); assertThat(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)).exists(); - } - - @ParameterizedTest - @ValueSource(strings = { "https://some-code-repository", "ssh://some-settings-repository" }) - void testWarningWhenRepoDoesNotMeetNamingConvention(String invalidRepo, @TempDir Path tempDir) { - // arrange - ProcessContextGitMock gitMock = new ProcessContextGitMock(context, tempDir); - context.setProcessContext(gitMock); - CreateCommandlet cc = context.getCommandletManager().getCommandlet(CreateCommandlet.class); - cc.newProject.setValueAsString(NEW_PROJECT_NAME, context); - cc.codeRepositoryFlag.setValue(!invalidRepo.contains("code")); // raise conflict - cc.settingsRepo.setValue(invalidRepo); - cc.skipTools.setValue(true); - context.setAnswers("yes"); - // act - cc.run(); - // assert - assertThat(context).logAtInteraction().hasMessageContaining("Do you really want to create the project?"); - Path newProjectPath = context.getIdeRoot().resolve(NEW_PROJECT_NAME); - assertThat(newProjectPath).exists(); - assertThat(context.getIdeHome()).isEqualTo(newProjectPath); - assertThat(newProjectPath.resolve(IdeContext.FOLDER_PLUGINS)).exists(); - assertThat(newProjectPath.resolve(IdeContext.FOLDER_SOFTWARE)).exists(); - assertThat(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)).exists(); - } - - @Test - void testWarningWhenCodeRepoUsingDefaultMark(@TempDir Path tempDir) { - String invalidCodeRepo = "-"; - // arrange - ProcessContextGitMock gitMock = new ProcessContextGitMock(context, tempDir); - context.setProcessContext(gitMock); - CreateCommandlet cc = context.getCommandletManager().getCommandlet(CreateCommandlet.class); - cc.newProject.setValueAsString(NEW_PROJECT_NAME, context); - cc.settingsRepo.setValue(invalidCodeRepo); - cc.codeRepositoryFlag.setValue(true); - cc.skipTools.setValue(true); - context.setAnswers("https://some-code-repository"); - // act - cc.run(); - // assert - assertThat(context).logAtWarning().hasMessageContaining("'-' is found after '--code'. This is invalid."); - Path newProjectPath = context.getIdeRoot().resolve(NEW_PROJECT_NAME); - assertThat(newProjectPath).exists(); - assertThat(context.getIdeHome()).isEqualTo(newProjectPath); - assertThat(newProjectPath.resolve(IdeContext.FOLDER_PLUGINS)).exists(); - assertThat(newProjectPath.resolve(IdeContext.FOLDER_SOFTWARE)).exists(); - assertThat(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)).exists(); + assertThat(context.getIdeRoot().resolve("_ide/tmp/projects").resolve(NEW_PROJECT_NAME)).doesNotExist(); } @Test @@ -220,6 +170,28 @@ void testWelcomeMessageDisplayed() { assertThat(context).logAtInfo().hasMessageContaining("Welcome to your new IDEasy project!"); } + @Test + void testProjectWithInvalidRepositoryNotCreated() { + + // arrange - create a new project that is invalid (does not contain ide.properties file) + GitContextImplMock gitContextImplMock = new GitContextImplMock(context, TEST_RESOURCES.resolve("pypi")); + + context.setGitContext(gitContextImplMock); + CreateCommandlet cc = context.getCommandletManager().getCommandlet(CreateCommandlet.class); + cc.newProject.setValueAsString(NEW_PROJECT_NAME, context); + cc.settingsRepo.setValue(IdeContext.DEFAULT_SETTINGS_REPO_URL); + cc.skipTools.setValue(true); + + // act - run the create command + assertThatThrownBy(cc::run) + .isInstanceOf(CliException.class) + .hasMessageContaining( + "Settings repository integrity check failed: The given git repository URL does not point to a valid settings or code-settings repository. Please verify and try again."); + + // assert + assertThat(context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS).resolve(NEW_PROJECT_NAME)).doesNotExist(); + } + @Test void testCreateWithDashPlaceholderAsCliArgument() { // arrange - see https://github.com/devonfw/IDEasy/issues/2106 @@ -234,7 +206,7 @@ void testCreateWithDashPlaceholderAsCliArgument() { assertThat(result).isEqualTo(0); assertThat(context).logAtError().hasNoMessageContaining("not found for commandlet"); assertThat(context).logAtInfo() - .hasMessageContaining("'-' was found for settings repository, the default settings repository"); + .hasMessageContaining("'-' was found for the repository, the default settings repository"); Path newProjectPath = context.getIdeRoot().resolve(NEW_PROJECT_NAME); assertThat(newProjectPath).exists(); } From b8bb02f80eba1f6288ce8bd712d2143d0ac54a7b Mon Sep 17 00:00:00 2001 From: laim2003 Date: Mon, 17 Aug 2026 14:49:39 +0200 Subject: [PATCH 51/89] #1695: corrected maven checkstyle recommendations Signed-off-by: laim2003 --- .../tools/ide/commandlet/AbstractUpdateCommandlet.java | 6 ++++-- .../devonfw/tools/ide/commandlet/CreateCommandletTest.java | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java index e6b049825f..5d160ce557 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java @@ -258,7 +258,8 @@ private void checkIntegrityAndMove(Path projectPath, String gitProjectName) { } case CODE_SETTINGS_COMBINED -> { - //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. (Formerly managed by the obsolete "--code" flag) + //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. + //(Formerly managed by the obsolete "--code" flag) targetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); moveProject(projectPath, targetDirectory); @@ -271,7 +272,8 @@ private void checkIntegrityAndMove(Path projectPath, String gitProjectName) { default -> { fileAccess.backup(projectPath); throw new CliException(getIntegrityCheckErrorMessage(String.format( - "The given git repository URL does not point to a valid settings or code-settings repository. Please verify and try again. Before trying again, please delete the folder %s", + "The given git repository URL does not point to a valid settings or code-settings repository. " + + "Please verify and try again. Before trying again, please delete the folder %s", this.context.getIdeHome()))); } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java index 06b3b5ca4c..bc65c8bdfc 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java @@ -186,7 +186,8 @@ void testProjectWithInvalidRepositoryNotCreated() { assertThatThrownBy(cc::run) .isInstanceOf(CliException.class) .hasMessageContaining( - "Settings repository integrity check failed: The given git repository URL does not point to a valid settings or code-settings repository. Please verify and try again."); + "Settings repository integrity check failed: " + + "The given git repository URL does not point to a valid settings or code-settings repository. Please verify and try again."); // assert assertThat(context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS).resolve(NEW_PROJECT_NAME)).doesNotExist(); From 55d081aae14af1503f4d21bc3f5eec08f15dcb03 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Mon, 17 Aug 2026 15:16:53 +0200 Subject: [PATCH 52/89] #1695: formatting corrections Signed-off-by: laim2003 --- .../tools/ide/commandlet/AbstractUpdateCommandlet.java | 7 +++++-- .../devonfw/tools/ide/git/repository/RepositoryUtil.java | 7 ++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java index 5d160ce557..403d8ff472 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java @@ -185,8 +185,11 @@ private void updateSettingsInStep(boolean codeRepository) { if (!this.context.getFileAccess().isEmptyDir(settingsPath)) { // settings folder seems to be invalid this.context.askToContinue( - "Your settings repository seems to be broken ('.git' folder not present). " + "We can fix this by moving your settings the backed up. " - + "You will be asked for the settings git URL and your settings will be cloned from scratch. " + "Do you want to proceed?"); + "Your settings repository seems to be broken ('.git' folder not present). " + + "We can fix this by moving your settings the backed up. " + + "You will be asked for the settings git URL and your settings will be cloned from scratch. " + + "Do you want to proceed?" + ); } this.context.getFileAccess().backup(settingsPath); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 9a6be63c13..7a3c5c0099 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -26,11 +26,8 @@ public static RepositoryType getRepositoryType(Path repositoryPath, String gitPr || Files.exists(repositoryPath.resolve(EnvironmentVariables.LEGACY_PROPERTIES))) { return RepositoryType.SETTINGS; } else if (gitProjectName != null - && Files.exists( - repositoryPath.resolve(IdeContext.FOLDER_SETTINGS)) - && getRepositoryType( - repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), - gitProjectName) == RepositoryType.SETTINGS) { + && Files.exists(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS)) + && getRepositoryType(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), gitProjectName) == RepositoryType.SETTINGS) { return RepositoryType.CODE_SETTINGS_COMBINED; } else if (!Files.exists(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS))) { return RepositoryType.CODE; From 2d766ddab8b0fa332cd09792c3914d2efa1341ad Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 10:54:01 +0200 Subject: [PATCH 53/89] #1695: migrated settings update logic into its own class. Signed-off-by: laim2003 --- .../commandlet/update/SettingsUpdater.java | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java new file mode 100644 index 0000000000..2f78ef7460 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java @@ -0,0 +1,206 @@ +package com.devonfw.tools.ide.commandlet.update; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.devonfw.tools.ide.cli.CliRethrowException; +import com.devonfw.tools.ide.context.AbstractIdeContext; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.git.GitContext; +import com.devonfw.tools.ide.git.GitUrl; +import com.devonfw.tools.ide.git.repository.RepositoryType; +import com.devonfw.tools.ide.git.repository.RepositoryUtil; +import com.devonfw.tools.ide.io.FileAccess; +import com.devonfw.tools.ide.property.StringProperty; + +/** + * Handles updating/cloning of the settings repository. + * Returns a result indicating the outcome of the settings update operation. + */ +public class SettingsUpdater { + + private static final Logger LOG = LoggerFactory.getLogger(SettingsUpdater.class); + + private final AbstractIdeContext context; + private final StringProperty settingsRepoProperty; + + /** + * Result of the settings update operation. + */ + public enum ResultStatus { + /** Settings repository was updated via pull and is valid. */ + SETTINGS_UPDATED, + /** Settings repository was cloned from scratch (blank state). */ + SETTINGS_CLONED, + /** Settings update failed (could not clone or invalid repository). */ + SETTINGS_UPDATE_FAILED + } + + /** + * Result object containing the outcome and repository type. + */ + public record SettingsUpdateResult(ResultStatus status, RepositoryType repositoryType) { + } + + /** + * Creates a new SettingsUpdater. + * + * @param context the IDE context + * @param settingsRepoProperty the settings repository property from the update commandlet + */ + public SettingsUpdater(AbstractIdeContext context, StringProperty settingsRepoProperty) { + this.context = context; + this.settingsRepoProperty = settingsRepoProperty; + } + + /** + * Updates the settings repository by either pulling (if exists) or cloning (if new). + * + * @param codeRepository whether this is a code repository (skip pull if true and not forced) + * @return the result of the settings update operation + */ + public SettingsUpdateResult updateSettings(boolean codeRepository) { + + Path settingsPath = this.context.getSettingsPath(); + boolean isSettingsRepo = this.context.getGitContext().isGitRepo(settingsPath); + + // If it's a code repository and not forced, skip the pull + if (codeRepository && isSettingsRepo && !this.context.isForceMode()) { + LOG.info("Skipping git pull in settings due to code repository. Use --force-pull to enforce pulling."); + return new SettingsUpdateResult(ResultStatus.SETTINGS_UPDATED, RepositoryType.SETTINGS); + } + + if (isSettingsRepo) { + // Existing settings repository - pull updates + return pullExistingSettings(settingsPath); + } else { + // No existing settings - clone from scratch + return cloneSettings(); + } + } + + private SettingsUpdateResult pullExistingSettings(Path settingsPath) { + + GitContext gitContext = this.context.getGitContext(); + if (gitContext.hasUntrackedFiles(settingsPath)) { + gitContext.pullSafelyWithStash(settingsPath); + } else { + gitContext.pull(settingsPath); + } + this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); + return new SettingsUpdateResult(ResultStatus.SETTINGS_UPDATED, RepositoryType.SETTINGS); + } + + private SettingsUpdateResult cloneSettings() { + + try { + // Get settings URL + GitUrl gitUrl = getOrAskSettingsUrl(); + + // Use unique temp directory to avoid leftovers from previous attempts + Path tempProjectPath = createUniqueTempProjectPath(); + this.context.getGitContext().pullOrClone(gitUrl, tempProjectPath); + return checkIntegrityAndMove(tempProjectPath, gitUrl.getProjectName()); + } catch (Exception e) { + throw new CliRethrowException("Settings repository integrity check failed: " + e.getMessage(), e); + } + } + + private GitUrl getOrAskSettingsUrl() { + + String repository = this.settingsRepoProperty.getValue(); + repository = handleDefaultRepository(repository); + String userPrompt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; + String defaultUrl = IdeContext.DEFAULT_SETTINGS_REPO_URL; + LOG.info(AbstractUpdateCommandlet.MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath()); + + GitUrl gitUrl = null; + if (repository != null) { + gitUrl = GitUrl.of(repository); + } + while ((gitUrl == null) || !gitUrl.isValid()) { + repository = this.context.askForInput(userPrompt, defaultUrl); + repository = handleDefaultRepository(repository); + gitUrl = GitUrl.of(repository); + if (!gitUrl.isValid()) { + LOG.warn("The input URL is not valid, please try again."); + } + } + return gitUrl; + } + + private Path createUniqueTempProjectPath() { + + // Use FileAccess.createTempDir to ensure unique directory and avoid leftovers + FileAccess fileAccess = this.context.getFileAccess(); + Path tempProjectsDir = this.context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS); + fileAccess.mkdirs(tempProjectsDir); + return fileAccess.createTempDir(this.context.getProjectName() + "-"); + } + + private SettingsUpdateResult checkIntegrityAndMove(Path projectPath, String gitProjectName) { + + FileAccess fileAccess = this.context.getFileAccess(); + + if (!Files.exists(projectPath)) { + throw new CliRethrowException("Git pull target folder does not exist."); + } + + Path targetDirectory; + RepositoryType repoType = RepositoryUtil.getRepositoryType(projectPath, gitProjectName); + + switch (repoType) { + case SETTINGS -> { + targetDirectory = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); + moveProject(projectPath, targetDirectory); + this.context.getGitContext().saveCurrentCommitId(targetDirectory, this.context.getSettingsCommitIdPath()); + return new SettingsUpdateResult(ResultStatus.SETTINGS_CLONED, RepositoryType.SETTINGS); + } + case CODE_SETTINGS_COMBINED -> { + // Special case: symlink from IDE_HOME/settings to workspace/repo_name/settings + targetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); + moveProject(projectPath, targetDirectory); + + Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); + Path symlinkTargetPath = this.context.getWorkspacePath().resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS); + + fileAccess.symlink(symlinkTargetPath, symlinkPath); + this.context.getGitContext().saveCurrentCommitId(symlinkTargetPath, this.context.getSettingsCommitIdPath()); + return new SettingsUpdateResult(ResultStatus.SETTINGS_CLONED, RepositoryType.CODE_SETTINGS_COMBINED); + } + default -> { + fileAccess.backup(projectPath); + throw new CliRethrowException(getIntegrityCheckErrorMessage(String.format( + "The given git repository URL does not point to a valid settings or code-settings repository. " + + "Please verify and try again. Before trying again, please delete the folder %s", + this.context.getIdeHome()))); + } + } + } + + private Path moveProject(Path from, Path to) { + + FileAccess fileAccess = this.context.getFileAccess(); + try { + fileAccess.move(from, to); + } catch (Exception e) { + throw new CliRethrowException(String.format("Failed to move project from %s to %s", from, to), e); + } + return to; + } + + private String getIntegrityCheckErrorMessage(String message) { + return String.format("Settings repository integrity check failed: %s", message); + } + + private String handleDefaultRepository(String repository) { + if ("-".equals(repository)) { + 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; + } +} From 5f6b49200ae996d1178511cb1d23ef1a28e5486c Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 10:58:26 +0200 Subject: [PATCH 54/89] #1695: migrated settings update logic into SettingsUpdater class, added recursivity check to RepositoryUtil Signed-off-by: laim2003 --- .../tools/ide/cli/CliRethrowException.java | 31 ++++ .../ide/commandlet/CommandletManagerImpl.java | 1 + .../ide/commandlet/CreateCommandlet.java | 1 + .../AbstractUpdateCommandlet.java | 148 +++--------------- .../commandlet/update/SettingsUpdater.java | 8 +- .../{ => update}/UpdateCommandlet.java | 3 +- .../tools/ide/context/AbstractIdeContext.java | 2 +- .../devonfw/tools/ide/context/IdeContext.java | 3 +- .../ide/git/repository/RepositoryType.java | 19 +++ .../ide/git/repository/RepositoryUtil.java | 41 +++-- .../ide/commandlet/UpdateCommandletTest.java | 1 + 11 files changed, 108 insertions(+), 150 deletions(-) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java rename cli/src/main/java/com/devonfw/tools/ide/commandlet/{ => update}/AbstractUpdateCommandlet.java (67%) rename cli/src/main/java/com/devonfw/tools/ide/commandlet/{ => update}/UpdateCommandlet.java (85%) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java new file mode 100644 index 0000000000..4d3ad013a6 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java @@ -0,0 +1,31 @@ +package com.devonfw.tools.ide.cli; + + +/** + * {@link CliException} that is thrown to immediately abort the CLI process when a critical guardrail fails + * (e.g., settings repository cannot be cloned or validated). This ensures the process stops rather than + * continuing in an invalid state. + */ +public final class CliRethrowException extends CliException { + + /** + * The constructor. + * + * @param message the {@link #getMessage() message}. + */ + public CliRethrowException(String message) { + + super(message); + } + + /** + * The constructor. + * + * @param message the {@link #getMessage() message}. + * @param cause the {@link #getCause() cause}. + */ + public CliRethrowException(String message, Throwable cause) { + + super(message, cause); + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java index f98a63decd..d115fefd17 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java @@ -14,6 +14,7 @@ import com.devonfw.tools.ide.cli.CliArgument; import com.devonfw.tools.ide.cli.CliArguments; import com.devonfw.tools.ide.commandlet.cleanup.CleanupCommandlet; +import com.devonfw.tools.ide.commandlet.update.UpdateCommandlet; import com.devonfw.tools.ide.completion.CompletionCandidateCollector; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.git.repository.RepositoryCommandlet; diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index ed2e937404..c04dbb71a0 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -7,6 +7,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.devonfw.tools.ide.commandlet.update.AbstractUpdateCommandlet; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.log.IdeLogLevel; diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java similarity index 67% rename from cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java rename to cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 403d8ff472..269bef49a3 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -1,4 +1,4 @@ -package com.devonfw.tools.ide.commandlet; +package com.devonfw.tools.ide.commandlet.update; import java.io.IOException; import java.nio.file.Files; @@ -12,14 +12,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.devonfw.tools.ide.cli.CliException; +import com.devonfw.tools.ide.cli.CliRethrowException; +import com.devonfw.tools.ide.commandlet.Commandlet; +import com.devonfw.tools.ide.commandlet.CommandletManager; +import com.devonfw.tools.ide.commandlet.CreateCommandlet; import com.devonfw.tools.ide.context.AbstractIdeContext; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.context.IdeStartContextImpl; -import com.devonfw.tools.ide.git.GitContext; -import com.devonfw.tools.ide.git.GitUrl; import com.devonfw.tools.ide.git.repository.RepositoryCommandlet; -import com.devonfw.tools.ide.git.repository.RepositoryUtil; import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.property.FlagProperty; import com.devonfw.tools.ide.property.StringProperty; @@ -44,17 +44,6 @@ public abstract class AbstractUpdateCommandlet extends Commandlet { private static final Logger LOG = LoggerFactory.getLogger(AbstractUpdateCommandlet.class); - private static final String MESSAGE_CODE_REPO_URL = """ - No code repository was given after '--code'. - Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc - Please enter the code repository below that includes your settings folder."""; - - private static final String MESSAGE_SETTINGS_REPO_URL = """ - No settings found at {} and no SETTINGS_URL is defined. - Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc - Please contact the technical lead of your project to get the SETTINGS_URL for your project to enter. - In case you just want to test IDEasy you may simply hit return to install the default settings."""; - /** {@link StringProperty} for the settings repository URL. */ public final StringProperty settingsRepo; @@ -168,7 +157,7 @@ protected void updateSettings() { LOG.info("Skipping git pull in settings due to code repository. Use --force-pull to enforce pulling."); return; } - this.context.newStep(getStepMessage()).run(() -> updateSettingsInStep(codeRepository), true); + this.context.newStep(getStepMessage()).run(this::updateSettingsInStep, true); } protected String getStepMessage() { @@ -176,127 +165,26 @@ protected String getStepMessage() { return "update (pull) settings repository"; } - private void updateSettingsInStep(boolean codeRepository) { - Path settingsPath = this.context.getSettingsPath(); - if (!codeRepository) { - boolean settingsRepository = this.context.getGitContext().isGitRepo(settingsPath); - if (!settingsRepository) { - if (Files.exists(settingsPath)) { - if (!this.context.getFileAccess().isEmptyDir(settingsPath)) { - // settings folder seems to be invalid - this.context.askToContinue( - "Your settings repository seems to be broken ('.git' folder not present). " - + "We can fix this by moving your settings the backed up. " - + "You will be asked for the settings git URL and your settings will be cloned from scratch. " - + "Do you want to proceed?" - ); - } - this.context.getFileAccess().backup(settingsPath); - } - //settings folder does not exist (yet), lets retrieve the settings url to pull - GitUrl gitUrl = getOrAskSettingsUrl(); - pullAndCheckIntegrity(gitUrl); - return; - } - } - GitContext gitContext = this.context.getGitContext(); - if (gitContext.hasUntrackedFiles(settingsPath)) { - gitContext.pullSafelyWithStash(settingsPath); - } else { - gitContext.pull(settingsPath); - } - this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); - } - - private GitUrl getOrAskSettingsUrl() { - - String repository = this.settingsRepo.getValue(); - repository = handleDefaultRepository(repository); - String userPrompt = "Settings 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); - } - while ((gitUrl == null) || !gitUrl.isValid()) { - repository = this.context.askForInput(userPrompt, defaultUrl); - repository = handleDefaultRepository(repository); - gitUrl = GitUrl.of(repository); - if (!gitUrl.isValid()) { - LOG.warn("The input URL is not valid, please try again."); - } - } - return gitUrl; - } - - /** - * We pull the settings repo from the remote into a temporary folder to perform health checks. - */ - private void pullAndCheckIntegrity(GitUrl gitUrl) { - GitContext gitContext = this.context.getGitContext(); - Path tempProjectPath = this.context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS).resolve(this.context.getProjectName()); - - gitContext.pullOrClone(gitUrl, tempProjectPath); - - checkIntegrityAndMove(tempProjectPath, gitUrl.getProjectName()); - } - - private void checkIntegrityAndMove(Path projectPath, String gitProjectName) { - - FileAccess fileAccess = this.context.getFileAccess(); - - if (!Files.exists(projectPath)) { - throw new CliException(getIntegrityCheckErrorMessage("Git pull target folder does not exist.")); - } + private void updateSettingsInStep() { + boolean codeRepository = this.context.isSettingsCodeRepository(); - Path targetDirectory; - switch (RepositoryUtil.getRepositoryType(projectPath, gitProjectName)) { - case SETTINGS -> { + SettingsUpdater settingsUpdater = new SettingsUpdater((AbstractIdeContext) this.context, this.settingsRepo); + SettingsUpdater.SettingsUpdateResult result = settingsUpdater.updateSettings(codeRepository); - targetDirectory = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); - moveProject(projectPath, targetDirectory); - this.context.getGitContext().saveCurrentCommitId(targetDirectory, this.context.getSettingsCommitIdPath()); + // Handle the result + switch (result.status()) { + case SETTINGS_UPDATED -> { + LOG.info("Settings repository updated successfully (type: {}).", result.repositoryType()); } - case CODE_SETTINGS_COMBINED -> { - - //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. - //(Formerly managed by the obsolete "--code" flag) - targetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); - moveProject(projectPath, targetDirectory); - - Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); - Path symlinkTargetPath = this.context.getWorkspacePath().resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS); - - fileAccess.symlink(symlinkTargetPath, symlinkPath); - this.context.getGitContext().saveCurrentCommitId(symlinkTargetPath, this.context.getSettingsCommitIdPath()); + case SETTINGS_CLONED -> { + LOG.info("Settings repository cloned successfully (type: {}).", result.repositoryType()); } - default -> { - fileAccess.backup(projectPath); - throw new CliException(getIntegrityCheckErrorMessage(String.format( - "The given git repository URL does not point to a valid settings or code-settings repository. " - + "Please verify and try again. Before trying again, please delete the folder %s", - this.context.getIdeHome()))); + case SETTINGS_UPDATE_FAILED -> { + throw new CliRethrowException("Settings repository update failed (type: " + result.repositoryType() + ")"); } } } - private Path moveProject(Path from, Path to) { - - FileAccess fileAccess = this.context.getFileAccess(); - try { - fileAccess.move(from, to); - } catch (Exception e) { - throw new CliException(getIntegrityCheckErrorMessage(String.format("Failed to move project from %s to %s", from, to)), e); - } - return to; - } - - private String getIntegrityCheckErrorMessage(String message) { - return String.format("Settings repository integrity check failed: %s", message); - } - private String handleDefaultRepository(String repository) { if ("-".equals(repository)) { LOG.info("'-' was found for the repository, the default settings repository '{}' will be used.", IdeContext.DEFAULT_SETTINGS_REPO_URL); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java index 2f78ef7460..4bf35a7ff3 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java @@ -27,6 +27,12 @@ public class SettingsUpdater { private final AbstractIdeContext context; private final StringProperty settingsRepoProperty; + private static final String MESSAGE_SETTINGS_REPO_URL = """ + No settings found at {} and no SETTINGS_URL is defined. + Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc + Please contact the technical lead of your project to get the SETTINGS_URL for your project to enter. + In case you just want to test IDEasy you may simply hit return to install the default settings."""; + /** * Result of the settings update operation. */ @@ -115,7 +121,7 @@ private GitUrl getOrAskSettingsUrl() { repository = handleDefaultRepository(repository); String userPrompt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; String defaultUrl = IdeContext.DEFAULT_SETTINGS_REPO_URL; - LOG.info(AbstractUpdateCommandlet.MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath()); + LOG.info(MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath()); GitUrl gitUrl = null; if (repository != null) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/UpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/UpdateCommandlet.java similarity index 85% rename from cli/src/main/java/com/devonfw/tools/ide/commandlet/UpdateCommandlet.java rename to cli/src/main/java/com/devonfw/tools/ide/commandlet/update/UpdateCommandlet.java index 944a4c0eeb..a0319dbb3b 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/UpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/UpdateCommandlet.java @@ -1,5 +1,6 @@ -package com.devonfw.tools.ide.commandlet; +package com.devonfw.tools.ide.commandlet.update; +import com.devonfw.tools.ide.commandlet.Commandlet; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.migration.IdeMigrator; diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java index 8dfcabaa17..0b430e4dcb 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java @@ -35,8 +35,8 @@ import com.devonfw.tools.ide.commandlet.CommandletManagerImpl; import com.devonfw.tools.ide.commandlet.ContextCommandlet; import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet; -import com.devonfw.tools.ide.commandlet.UpdateCommandlet; import com.devonfw.tools.ide.commandlet.UpgradeCommandlet; +import com.devonfw.tools.ide.commandlet.update.UpdateCommandlet; import com.devonfw.tools.ide.common.SystemPath; import com.devonfw.tools.ide.completion.CompletionCandidate; import com.devonfw.tools.ide.completion.CompletionCandidateCollector; diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java index 7516fae64d..013591e2a8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java @@ -11,6 +11,7 @@ import com.devonfw.tools.ide.cli.CliException; import com.devonfw.tools.ide.cli.CliOfflineException; import com.devonfw.tools.ide.commandlet.CommandletManager; +import com.devonfw.tools.ide.commandlet.update.AbstractUpdateCommandlet; import com.devonfw.tools.ide.common.SystemPath; import com.devonfw.tools.ide.environment.EnvironmentVariables; import com.devonfw.tools.ide.environment.EnvironmentVariablesType; @@ -69,7 +70,7 @@ public interface IdeContext extends IdeStartContext { /** * The default settings URL. * - * @see com.devonfw.tools.ide.commandlet.AbstractUpdateCommandlet + * @see AbstractUpdateCommandlet */ String DEFAULT_SETTINGS_REPO_URL = "https://github.com/devonfw/ide-settings.git"; diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java new file mode 100644 index 0000000000..81195bc178 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java @@ -0,0 +1,19 @@ +package com.devonfw.tools.ide.git.repository; + +/** + * Enum representation of a detected {@link RepositoryType}. + */ +public enum RepositoryType { + + /** Git Repository is a code repository. */ + CODE, + + /** Git Repository is a settings repository. */ + SETTINGS, + + /** A combined code & settings repository contains both the settings-folder and the code within the workspace folder. */ + CODE_SETTINGS_COMBINED, + + /** The type of the repository could not be determined. */ + UNKNOWN +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 7a3c5c0099..23c6a3663c 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -6,18 +6,39 @@ import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.environment.EnvironmentVariables; -/// Utility class for IDEasy settings/code repositories +/** + * Utility class for IDEasy settings/code repositories. + */ public class RepositoryUtil { /** - * Checks whether te given git repository is a settings repository, a combined settings and code repository, or a typical code repositor. Combined code and + * Checks whether the given git repository is a settings repository, a combined settings and code repository, or a typical code repository. Combined code and * settings repository is detected by checking whether IDE_HOME/workspaces/main/[gitProjectName]/settings exists and is a valid settings repository. * - * @param repositoryPath - The path of the repository to be checked. + * @param repositoryPath the path of the repository to be checked. + * @param gitProjectName the name of the git project. * @return {@link RepositoryType} of the repository. */ public static RepositoryType getRepositoryType(Path repositoryPath, String gitProjectName) { + return getRepositoryType(repositoryPath, gitProjectName, 0); + } + + /** + * Internal recursive method with depth tracking to prevent infinite recursion. + * + * @param repositoryPath the path of the repository to be checked. + * @param gitProjectName the name of the git project. + * @param depth the current recursion depth. + * @return {@link RepositoryType} of the repository. + */ + private static RepositoryType getRepositoryType(Path repositoryPath, String gitProjectName, int depth) { + + // Prevent infinite recursion by limiting depth (max 2 levels: root -> settings) + if (depth > 2) { + return RepositoryType.UNKNOWN; + } + if (!Files.exists(repositoryPath)) { return RepositoryType.UNKNOWN; } @@ -27,23 +48,11 @@ public static RepositoryType getRepositoryType(Path repositoryPath, String gitPr return RepositoryType.SETTINGS; } else if (gitProjectName != null && Files.exists(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS)) - && getRepositoryType(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), gitProjectName) == RepositoryType.SETTINGS) { + && getRepositoryType(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), gitProjectName, depth + 1) == RepositoryType.SETTINGS) { return RepositoryType.CODE_SETTINGS_COMBINED; } else if (!Files.exists(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS))) { return RepositoryType.CODE; } return RepositoryType.UNKNOWN; } - - /// enum representation of a detected {@link RepositoryType} - public enum RepositoryType { - /// Git Repository is a code repository. - CODE, - /// Git Repository is a settings repository. - SETTINGS, - /// A combined code & settings repository contains both the settings-folder and the code within the workspace folder. - CODE_SETTINGS_COMBINED, - /// The type of the repository could not be determined. - UNKNOWN - } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java index 626d0b0aa4..cb23034a64 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import com.devonfw.tools.ide.commandlet.update.UpdateCommandlet; import com.devonfw.tools.ide.context.AbstractIdeContextTest; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.context.IdeTestContext; From 1ce3a98bcc90387cf5bef6e1cdaa632fabb7aa48 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 13:14:34 +0200 Subject: [PATCH 55/89] #1695: only create project structure when health check succeeded Signed-off-by: laim2003 --- .../tools/ide/commandlet/CreateCommandlet.java | 17 ++++++++++++++--- .../update/AbstractUpdateCommandlet.java | 8 -------- .../update/SettingsUpdateResultStatus.java | 4 ++++ .../ide/commandlet/update/SettingsUpdater.java | 11 ++++++++--- 4 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdateResultStatus.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index c04dbb71a0..673a1fe598 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -57,13 +57,24 @@ protected void doRun() { 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); - } else { - this.context.getFileAccess().mkdirs(newProjectPath); } + // First run the settings update (super.doRun()) to validate the settings repository + // Only if that succeeds, we create the project structure + try { + super.doRun(); + } catch (Exception e) { + // If settings update fails, clean up any temp directories and rethrow + throw e; + } + + // Settings update succeeded, now create the project structure + if (!this.context.getFileAccess().isEmptyDir(newProjectPath)) { + this.context.getFileAccess().backup(newProjectPath); + } + this.context.getFileAccess().mkdirs(newProjectPath); initializeProject(newProjectPath); this.context.setIdeHome(newProjectPath); - super.doRun(); this.context.getFileAccess().writeFileContent(IdeVersion.getVersionString(), newProjectPath.resolve(IdeContext.FILE_SOFTWARE_VERSION)); IdeLogLevel.SUCCESS.log(LOG, "Successfully created new project '{}'.", newProjectName); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 269bef49a3..d69628e2f4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -185,14 +185,6 @@ private void updateSettingsInStep() { } } - private String handleDefaultRepository(String repository) { - if ("-".equals(repository)) { - 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 updateSoftware() { if (this.skipTools.isTrue()) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdateResultStatus.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdateResultStatus.java new file mode 100644 index 0000000000..4bdf281b7e --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdateResultStatus.java @@ -0,0 +1,4 @@ +package com.devonfw.tools.ide.commandlet.update; + +public enum SettingsUpdateResultStatus { +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java index 4bf35a7ff3..14b7b6edff 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java @@ -17,8 +17,7 @@ import com.devonfw.tools.ide.property.StringProperty; /** - * Handles updating/cloning of the settings repository. - * Returns a result indicating the outcome of the settings update operation. + * Handles updating/cloning of the settings repository. Returns a result indicating the outcome of the settings update operation. */ public class SettingsUpdater { @@ -49,6 +48,7 @@ public enum ResultStatus { * Result object containing the outcome and repository type. */ public record SettingsUpdateResult(ResultStatus status, RepositoryType repositoryType) { + } /** @@ -102,15 +102,20 @@ private SettingsUpdateResult pullExistingSettings(Path settingsPath) { private SettingsUpdateResult cloneSettings() { + Path tempProjectPath = null; try { // Get settings URL GitUrl gitUrl = getOrAskSettingsUrl(); // Use unique temp directory to avoid leftovers from previous attempts - Path tempProjectPath = createUniqueTempProjectPath(); + tempProjectPath = createUniqueTempProjectPath(); this.context.getGitContext().pullOrClone(gitUrl, tempProjectPath); return checkIntegrityAndMove(tempProjectPath, gitUrl.getProjectName()); } catch (Exception e) { + // Clean up temp directory on failure + if (tempProjectPath != null) { + this.context.getFileAccess().backup(tempProjectPath); + } throw new CliRethrowException("Settings repository integrity check failed: " + e.getMessage(), e); } } From 572892d50a0d90d81644c5d51039ee5ba1e04db9 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:18:07 +0200 Subject: [PATCH 56/89] #1695: extended CliException with isForceRethrowInStep() flag --- .../com/devonfw/tools/ide/cli/CliException.java | 11 +++++++++++ .../devonfw/tools/ide/cli/CliRethrowException.java | 13 +++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliException.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliException.java index 16cb0598fe..3a39a319a4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/cli/CliException.java +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliException.java @@ -64,4 +64,15 @@ public int getExitCode() { return this.exitCode; } + /** + * @return {@code true} if this exception has to be re-thrown from a {@link com.devonfw.tools.ide.step.Step Step} even if that {@code Step} was not asked to + * re-throw errors, {@code false} otherwise (default). A regular error only makes the according {@code Step} fail while the overall process continues with + * the next step. However, if a critical guardrail was violated (e.g. no valid settings could be established) continuing makes no sense and the entire + * process has to be aborted. + */ + public boolean isForceRethrowInStep() { + + return false; + } + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java index 4d3ad013a6..fc991233bd 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java @@ -1,10 +1,9 @@ package com.devonfw.tools.ide.cli; - /** - * {@link CliException} that is thrown to immediately abort the CLI process when a critical guardrail fails - * (e.g., settings repository cannot be cloned or validated). This ensures the process stops rather than - * continuing in an invalid state. + * {@link CliException} that aborts the entire CLI process when a critical guardrail fails (e.g. the settings repository could not be cloned or is not a valid + * settings repository). Unlike a regular error that only makes the current {@link com.devonfw.tools.ide.step.Step Step} fail while the overall process + * continues, this exception {@link #isForceRethrowInStep() is always re-thrown} so no further step is executed in an invalid state. */ public final class CliRethrowException extends CliException { @@ -28,4 +27,10 @@ public CliRethrowException(String message, Throwable cause) { super(message, cause); } + + @Override + public boolean isForceRethrowInStep() { + + return true; + } } From 0c99b284c8a026636d270b696d1a79341c7f2b7b Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:21:55 +0200 Subject: [PATCH 57/89] #1695: CreateCommandlet now only creates the project structure after the health checks succeeded. --- .../ide/commandlet/CreateCommandlet.java | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index 673a1fe598..bc95410bd5 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -51,42 +51,43 @@ public boolean isIdeHomeRequired() { @Override protected void doRun() { - String newProjectName = this.newProject.getValue(); - Path newProjectPath = this.context.getIdeRoot().resolve(newProjectName); - + Path newProjectPath = getNewProjectPath(); LOG.info("Creating new IDEasy project in {}", newProjectPath); - if (!this.context.getFileAccess().isEmptyDir(newProjectPath)) { + FileAccess fileAccess = this.context.getFileAccess(); + if (!fileAccess.isEmptyDir(newProjectPath)) { this.context.askToContinue("Directory {} already exists. Do you want to continue?", newProjectPath); + fileAccess.backup(newProjectPath); } + // point IDE_HOME to the new project before the settings are checked - this only computes the paths and creates nothing on disk so that a failing + // health check leaves no project behind. As IDE_HOME/settings does not exist yet the settings will be cloned instead of pulled. + this.context.setIdeHome(newProjectPath); + super.doRun(); + } - // First run the settings update (super.doRun()) to validate the settings repository - // Only if that succeeds, we create the project structure - try { - super.doRun(); - } catch (Exception e) { - // If settings update fails, clean up any temp directories and rethrow - throw e; - } + @Override + protected void prepareProject() { - // Settings update succeeded, now create the project structure - if (!this.context.getFileAccess().isEmptyDir(newProjectPath)) { - this.context.getFileAccess().backup(newProjectPath); - } - this.context.getFileAccess().mkdirs(newProjectPath); - initializeProject(newProjectPath); - this.context.setIdeHome(newProjectPath); - this.context.getFileAccess().writeFileContent(IdeVersion.getVersionString(), newProjectPath.resolve(IdeContext.FILE_SOFTWARE_VERSION)); - IdeLogLevel.SUCCESS.log(LOG, "Successfully created new project '{}'.", newProjectName); + // only called after the settings passed the health check + Path newProjectPath = getNewProjectPath(); + FileAccess fileAccess = this.context.getFileAccess(); + fileAccess.mkdirs(newProjectPath); + fileAccess.mkdirs(newProjectPath.resolve(IdeContext.FOLDER_SOFTWARE)); + fileAccess.mkdirs(newProjectPath.resolve(IdeContext.FOLDER_PLUGINS)); + fileAccess.mkdirs(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)); + } + @Override + protected void finalizeProject() { + + Path newProjectPath = getNewProjectPath(); + this.context.getFileAccess().writeFileContent(IdeVersion.getVersionString(), newProjectPath.resolve(IdeContext.FILE_SOFTWARE_VERSION)); + IdeLogLevel.SUCCESS.log(LOG, "Successfully created new project '{}'.", this.newProject.getValue()); logWelcomeMessage(); } - private void initializeProject(Path newInstancePath) { + private Path getNewProjectPath() { - FileAccess fileAccess = this.context.getFileAccess(); - fileAccess.mkdirs(newInstancePath.resolve(IdeContext.FOLDER_SOFTWARE)); - fileAccess.mkdirs(newInstancePath.resolve(IdeContext.FOLDER_PLUGINS)); - fileAccess.mkdirs(newInstancePath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)); + return this.context.getIdeRoot().resolve(this.newProject.getValue()); } @Override From 2edfe0eea4b0e737e56703a16325d3d72ff11e31 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:54:53 +0200 Subject: [PATCH 58/89] #1695: updated RepositoryUtil --- .../ide/git/repository/RepositoryUtil.java | 55 ++++++++----------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 23c6a3663c..2eebe1d75d 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -12,47 +12,38 @@ public class RepositoryUtil { /** - * Checks whether the given git repository is a settings repository, a combined settings and code repository, or a typical code repository. Combined code and - * settings repository is detected by checking whether IDE_HOME/workspaces/main/[gitProjectName]/settings exists and is a valid settings repository. + * Checks whether the given git repository is a settings repository, a combined settings and code repository, or a typical code repository. A combined code + * and settings repository is detected by a top-level {@code settings} folder that itself is a valid settings folder. * - * @param repositoryPath the path of the repository to be checked. - * @param gitProjectName the name of the git project. - * @return {@link RepositoryType} of the repository. + * @param repositoryPath the {@link Path} to the repository to check. + * @return the {@link RepositoryType} of the repository. */ - public static RepositoryType getRepositoryType(Path repositoryPath, String gitProjectName) { + public static RepositoryType getRepositoryType(Path repositoryPath) { - return getRepositoryType(repositoryPath, gitProjectName, 0); - } - - /** - * Internal recursive method with depth tracking to prevent infinite recursion. - * - * @param repositoryPath the path of the repository to be checked. - * @param gitProjectName the name of the git project. - * @param depth the current recursion depth. - * @return {@link RepositoryType} of the repository. - */ - private static RepositoryType getRepositoryType(Path repositoryPath, String gitProjectName, int depth) { - - // Prevent infinite recursion by limiting depth (max 2 levels: root -> settings) - if (depth > 2) { - return RepositoryType.UNKNOWN; - } - - if (!Files.exists(repositoryPath)) { + if (!Files.isDirectory(repositoryPath)) { return RepositoryType.UNKNOWN; } - - if (Files.exists(repositoryPath.resolve(EnvironmentVariables.DEFAULT_PROPERTIES)) - || Files.exists(repositoryPath.resolve(EnvironmentVariables.LEGACY_PROPERTIES))) { + if (isSettingsFolder(repositoryPath)) { return RepositoryType.SETTINGS; - } else if (gitProjectName != null - && Files.exists(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS)) - && getRepositoryType(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS), gitProjectName, depth + 1) == RepositoryType.SETTINGS) { + } + Path settingsFolder = repositoryPath.resolve(IdeContext.FOLDER_SETTINGS); + if (isSettingsFolder(settingsFolder)) { return RepositoryType.CODE_SETTINGS_COMBINED; - } else if (!Files.exists(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS))) { + } + if (!Files.exists(settingsFolder)) { return RepositoryType.CODE; } + // there is a settings folder but it does not contain the required properties file return RepositoryType.UNKNOWN; } + + /** + * @param folder the {@link Path} to check. + * @return {@code true} if the given {@code folder} is the root of a settings repository, {@code false} otherwise. + */ + private static boolean isSettingsFolder(Path folder) { + + return Files.exists(folder.resolve(EnvironmentVariables.DEFAULT_PROPERTIES)) + || Files.exists(folder.resolve(EnvironmentVariables.LEGACY_PROPERTIES)); + } } From 496d09d38e246e80e8315d07f9f9c41ee4773907 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:55:03 +0200 Subject: [PATCH 59/89] #1695: updated documentation --- documentation/settings.adoc | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/documentation/settings.adoc b/documentation/settings.adoc index 3d7a1540a1..3bf2f8e240 100644 --- a/documentation/settings.adoc +++ b/documentation/settings.adoc @@ -51,6 +51,21 @@ But we do not want to forget the following advantage: Anyhow you can still create feature branches in standalone settings repositories to manage such scenarios and follow KISS and trunk-based development so you more or less avoid such problems. However, if you are in a monolithic project with complex release branches you may consider using the "settings in code repository" approach. +== Health check + +Whenever `IDEasy` clones or updates your settings it first clones the git repository into a temporary directory and performs a health check on it: + +* the given git URL has to be valid, +* cloning the repository has to succeed, +* and the repository has to be a settings repository or a combined code and settings repository (see link:#code-repository[above]). + +Only if this health check succeeded the settings are installed: an existing settings repository is updated via `git pull` while a new one is moved from the temporary directory to its final location. +This way a broken or wrong git URL can never leave you with a damaged project. +In particular `ide create` will not create the project at all if the health check fails, so you can simply fix the URL and try again. + +If you are sure that you know better, you can use the `--force` option. +`IDEasy` will then still report the problem but ask you whether you want to continue anyway. + == Structure The settings folder has to follow this file structure: From 9aecc4083070ad62a817bd17b1b5e47a13d3bbde Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:56:36 +0200 Subject: [PATCH 60/89] #1695: updated CreateCommandlet --- .../tools/ide/commandlet/CreateCommandlet.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index bc95410bd5..67e2256983 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -62,6 +62,9 @@ protected void doRun() { // health check leaves no project behind. As IDE_HOME/settings does not exist yet the settings will be cloned instead of pulled. this.context.setIdeHome(newProjectPath); super.doRun(); + this.context.getFileAccess().writeFileContent(IdeVersion.getVersionString(), newProjectPath.resolve(IdeContext.FILE_SOFTWARE_VERSION)); + IdeLogLevel.SUCCESS.log(LOG, "Successfully created new project '{}'.", this.newProject.getValue()); + logWelcomeMessage(); } @Override @@ -76,15 +79,6 @@ protected void prepareProject() { fileAccess.mkdirs(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)); } - @Override - protected void finalizeProject() { - - Path newProjectPath = getNewProjectPath(); - this.context.getFileAccess().writeFileContent(IdeVersion.getVersionString(), newProjectPath.resolve(IdeContext.FILE_SOFTWARE_VERSION)); - IdeLogLevel.SUCCESS.log(LOG, "Successfully created new project '{}'.", this.newProject.getValue()); - logWelcomeMessage(); - } - private Path getNewProjectPath() { return this.context.getIdeRoot().resolve(this.newProject.getValue()); From c2aaaa058437cb35bcfa36fda89dc6d38cdfb555 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:58:08 +0200 Subject: [PATCH 61/89] #1695: divided AbstractUpdateCommandlet settings update step into verify & apply steps --- .../update/AbstractUpdateCommandlet.java | 52 ++++++++++++------- .../update/SettingsUpdateResultStatus.java | 4 -- 2 files changed, 34 insertions(+), 22 deletions(-) delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdateResultStatus.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index d69628e2f4..725254c3df 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -12,10 +12,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.devonfw.tools.ide.cli.CliRethrowException; import com.devonfw.tools.ide.commandlet.Commandlet; import com.devonfw.tools.ide.commandlet.CommandletManager; import com.devonfw.tools.ide.commandlet.CreateCommandlet; +import com.devonfw.tools.ide.commandlet.update.SettingsUpdater.ResultStatus; +import com.devonfw.tools.ide.commandlet.update.SettingsUpdater.SettingsUpdateResult; import com.devonfw.tools.ide.context.AbstractIdeContext; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.context.IdeStartContextImpl; @@ -97,6 +98,15 @@ protected void doRun() { createStartScripts(); } + /** + * Hook that is called after the settings passed the health check but before they are moved to their final location. Does nothing by default and is overridden + * by {@link CreateCommandlet} to create the project structure so that no project is created at all if the health check failed. + */ + protected void prepareProject() { + + // nothing to do by default + } + private void reloadContext() { ((AbstractIdeContext) this.context).reload(); @@ -148,16 +158,18 @@ private void setupConf(Path template, Path conf) { /** * Updates the settings repository in IDE_HOME/settings by either cloning if no such repository exists or pulling if the repository exists then saves the - * latest current commit ID in the file ".commit.id". + * latest current commit ID in the file ".commit.id". The settings are always cloned into a temporary directory first where a health check is performed. Only + * if that health check succeeded the settings are pulled or the verified clone is moved to its final location. */ protected void updateSettings() { boolean codeRepository = this.context.isSettingsCodeRepository(); - if (codeRepository && !(this.context.isForceMode() || forcePull.isTrue())) { + if (codeRepository && !(this.context.isForceMode() || this.forcePull.isTrue())) { LOG.info("Skipping git pull in settings due to code repository. Use --force-pull to enforce pulling."); return; } - this.context.newStep(getStepMessage()).run(this::updateSettingsInStep, true); + Step step = this.context.newStep(getStepMessage()); + step.run(() -> updateSettingsInStep(step)); } protected String getStepMessage() { @@ -165,23 +177,27 @@ protected String getStepMessage() { return "update (pull) settings repository"; } - private void updateSettingsInStep() { - boolean codeRepository = this.context.isSettingsCodeRepository(); - - SettingsUpdater settingsUpdater = new SettingsUpdater((AbstractIdeContext) this.context, this.settingsRepo); - SettingsUpdater.SettingsUpdateResult result = settingsUpdater.updateSettings(codeRepository); + private void updateSettingsInStep(Step step) { - // Handle the result - switch (result.status()) { - case SETTINGS_UPDATED -> { - LOG.info("Settings repository updated successfully (type: {}).", result.repositoryType()); - } - case SETTINGS_CLONED -> { - LOG.info("Settings repository cloned successfully (type: {}).", result.repositoryType()); + SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo); + try { + SettingsUpdateResult result = this.context.newStep("Performing health check on settings").call(settingsUpdater::checkSettings, () -> null); + // fatal problems (e.g. no valid settings at all) were already rethrown, so reaching this point means the settings we have stay usable + if (result == null) { + step.error("Health check on settings failed - the settings have not been updated."); + return; + } else if (result.status() == ResultStatus.SETTINGS_UPDATE_FAILED) { + step.error("The settings have not been updated: {}", result.errorMessage()); + return; } - case SETTINGS_UPDATE_FAILED -> { - throw new CliRethrowException("Settings repository update failed (type: " + result.repositoryType() + ")"); + prepareProject(); + boolean applied = this.context.newStep("Applying update").run(() -> settingsUpdater.applySettings(result)); + if (!applied) { + step.error("Failed to apply the settings update."); } + } finally { + // the verified clone lives across both steps and the prepareProject hook so it is only here that its lifetime ends + settingsUpdater.cleanup(); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdateResultStatus.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdateResultStatus.java deleted file mode 100644 index 4bdf281b7e..0000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdateResultStatus.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.devonfw.tools.ide.commandlet.update; - -public enum SettingsUpdateResultStatus { -} From 669bf24fac95c9c981cb9b319608edc3da737e98 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:58:57 +0200 Subject: [PATCH 62/89] #1695: updated GitContextMock to use the default settings repo URL as a mock URL (for testing new workflow) --- .../test/java/com/devonfw/tools/ide/git/GitContextMock.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java index 10a5e3f023..27723760ac 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java @@ -20,7 +20,8 @@ */ public class GitContextMock extends GitContextImpl { - private static final String MOCKED_URL_VALUE = "mocked url value"; + /** Fallback URL for repositories without a mocked {@code .git/config} - has to be a {@link GitUrl#isValid() valid} git URL. */ + private static final String MOCKED_URL_VALUE = DEFAULT_SETTINGS_GIT_URL; /** Filename used to persist mocked remotes inside the {@code .git} folder. */ private static final String REMOTES_FILE = "remotes.properties"; From 6c1c1233dcee9fdc98765af7c2b23a21c4ba973d Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:59:34 +0200 Subject: [PATCH 63/89] #1695: updated Step implementation to support isForceRethrow --- .../java/com/devonfw/tools/ide/step/Step.java | 29 ++++++++-- .../com/devonfw/tools/ide/step/StepTest.java | 57 +++++++++++++++++++ cli/src/test/resources/code-settings/pom.xml | 1 + .../code-settings/settings/ide.properties | 1 + 4 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 cli/src/test/resources/code-settings/pom.xml create mode 100644 cli/src/test/resources/code-settings/settings/ide.properties diff --git a/cli/src/main/java/com/devonfw/tools/ide/step/Step.java b/cli/src/main/java/com/devonfw/tools/ide/step/Step.java index 19760d9723..800caa95ab 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/step/Step.java +++ b/cli/src/main/java/com/devonfw/tools/ide/step/Step.java @@ -3,6 +3,8 @@ import java.util.concurrent.Callable; import java.util.function.Supplier; +import com.devonfw.tools.ide.cli.CliException; + /** * Interface for a {@link Step} of the process. Allows to split larger processes into smaller steps that are traced and measured. Also prevents that if one step * fails, the overall process can still continue so a sub-step (e.g. "plugin installation" or "git update") does not automatically block the entire process. At @@ -219,7 +221,8 @@ default boolean run(Runnable stepCode) { /** * @param stepCode the {@link Runnable} to {@link Runnable#run() execute} for this {@link Step}. - * @param rethrow - {@code true} to rethrow a potential {@link Throwable error}. + * @param rethrow - {@code true} to rethrow a potential {@link Throwable error}. Independent of this flag an error is always rethrown if it + * {@link CliException#isForceRethrowInStep() forces} it. * @return {@code true} on success, {@code false} on error (if {@code rethrow} is {@code false}). */ default boolean run(Runnable stepCode, boolean rethrow) { @@ -231,8 +234,10 @@ default boolean run(Runnable stepCode, boolean rethrow) { } return true; } catch (RuntimeException | Error e) { - error(e); - if (rethrow) { + boolean forceRethrow = isForceRethrow(e); + // if the error is rethrown it gets logged by the caller so we suppress duplicated error messages here + error(e, forceRethrow); + if (rethrow || forceRethrow) { throw e; } return false; @@ -264,7 +269,8 @@ default R call(Callable stepCode, Supplier resultOnErrorSupplier) { /** * @param stepCode the {@link Callable} to {@link Callable#call() execute} for this {@link Step}. - * @param rethrow - {@code true} to rethrow a potential {@link Throwable error}. + * @param rethrow - {@code true} to rethrow a potential {@link Throwable error}. Independent of this flag an error is always rethrown if it + * {@link CliException#isForceRethrowInStep() forces} it. * @param resultOnErrorSupplier the {@link Supplier} {@link Supplier#get() providing} the result to be returned in case of a {@link Throwable error}. * @param type of the return value. * @return the value returned from {@link Callable#call()}. @@ -278,8 +284,10 @@ default R call(Callable stepCode, boolean rethrow, Supplier resultOnEr } return result; } catch (Throwable e) { - error(e); - if (rethrow) { + boolean forceRethrow = isForceRethrow(e); + // if the error is rethrown it gets logged by the caller so we suppress duplicated error messages here + error(e, forceRethrow); + if (rethrow || forceRethrow) { if (e instanceof RuntimeException re) { throw re; } else if (e instanceof Error error) { @@ -294,4 +302,13 @@ default R call(Callable stepCode, boolean rethrow, Supplier resultOnEr } } + /** + * @param error the {@link Throwable} that occurred inside a {@link Step}. + * @return {@code true} if the given {@code error} has to be rethrown even if the {@link Step} was not asked to rethrow errors, {@code false} otherwise. + */ + private static boolean isForceRethrow(Throwable error) { + + return (error instanceof CliException cliException) && cliException.isForceRethrowInStep(); + } + } diff --git a/cli/src/test/java/com/devonfw/tools/ide/step/StepTest.java b/cli/src/test/java/com/devonfw/tools/ide/step/StepTest.java index 0a5b87d12c..bbc4d9332f 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/step/StepTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/step/StepTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Test; +import com.devonfw.tools.ide.cli.CliRethrowException; import com.devonfw.tools.ide.context.AbstractIdeContextTest; import com.devonfw.tools.ide.context.IdeTestContext; import com.devonfw.tools.ide.log.IdeLogEntry; @@ -130,4 +131,60 @@ void testInvalidUsageErrorSuccess() { IdeLogEntry.ofDebug("Step 'Test-Step' ended successfully.")); } + @Test + void testRunSwallowsRegularError() { + + // arrange + IdeTestContext context = newContext(PROJECT_BASIC, "project", false); + Step step = context.newStep("Test-Step"); + // act + boolean success = step.run(() -> { + throw new IllegalStateException("regular error"); + }); + // assert + assertThat(success).isFalse(); + assertThat(step.isFailure()).isTrue(); + } + + @Test + void testRunRethrowsForcedError() { + + // arrange + IdeTestContext context = newContext(PROJECT_BASIC, "project", false); + Step step = context.newStep("Test-Step"); + // act & assert + assertThatThrownBy(() -> step.run(() -> { + throw new CliRethrowException("fatal error"); + })).isInstanceOf(CliRethrowException.class).hasMessage("fatal error"); + assertThat(step.isFailure()).isTrue(); + } + + @Test + void testCallReturnsFallbackOnRegularError() { + + // arrange + IdeTestContext context = newContext(PROJECT_BASIC, "project", false); + Step step = context.newStep("Test-Step"); + // act + String result = step.call(() -> { + throw new IllegalStateException("regular error"); + }, () -> "fallback"); + // assert + assertThat(result).isEqualTo("fallback"); + assertThat(step.isFailure()).isTrue(); + } + + @Test + void testCallRethrowsForcedError() { + + // arrange + IdeTestContext context = newContext(PROJECT_BASIC, "project", false); + Step step = context.newStep("Test-Step"); + // act & assert + assertThatThrownBy(() -> step.call(() -> { + throw new CliRethrowException("fatal error"); + }, () -> "fallback")).isInstanceOf(CliRethrowException.class).hasMessage("fatal error"); + assertThat(step.isFailure()).isTrue(); + } + } diff --git a/cli/src/test/resources/code-settings/pom.xml b/cli/src/test/resources/code-settings/pom.xml new file mode 100644 index 0000000000..6d465deda5 --- /dev/null +++ b/cli/src/test/resources/code-settings/pom.xml @@ -0,0 +1 @@ +code diff --git a/cli/src/test/resources/code-settings/settings/ide.properties b/cli/src/test/resources/code-settings/settings/ide.properties new file mode 100644 index 0000000000..28913aee06 --- /dev/null +++ b/cli/src/test/resources/code-settings/settings/ide.properties @@ -0,0 +1 @@ +IDE_TOOLS=java,mvn From 71e74e1eb311272f40d6b227baca7a40e9a807c7 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 22:59:51 +0200 Subject: [PATCH 64/89] #1695: updated tests --- .../ide/commandlet/CreateCommandletTest.java | 55 +++++++++++++++++- .../ide/commandlet/UpdateCommandletTest.java | 56 +++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java index bc65c8bdfc..56e6f403cb 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java @@ -16,6 +16,7 @@ import com.devonfw.tools.ide.environment.EnvironmentVariables; import com.devonfw.tools.ide.environment.EnvironmentVariablesType; import com.devonfw.tools.ide.git.GitContextImplMock; +import com.devonfw.tools.ide.io.WindowsSymlinkTestHelper; import com.devonfw.tools.ide.version.IdeVersion; /** @@ -64,6 +65,8 @@ void testCreateCommandletRun() { assertThat(newProjectPath.resolve(IdeContext.FOLDER_PLUGINS)).exists(); assertThat(newProjectPath.resolve(IdeContext.FOLDER_SOFTWARE)).exists(); assertThat(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)).exists(); + // the settings have to be cloned into the new project and not into the project the create command was started from + assertThat(newProjectPath.resolve(IdeContext.FOLDER_SETTINGS).resolve("ide.properties")).exists(); assertThat(context.getIdeRoot().resolve("_ide/tmp/projects").resolve(NEW_PROJECT_NAME)).doesNotExist(); } @@ -189,10 +192,60 @@ void testProjectWithInvalidRepositoryNotCreated() { "Settings repository integrity check failed: " + "The given git repository URL does not point to a valid settings or code-settings repository. Please verify and try again."); - // assert + // assert - if "ide create" fails then no project shall be created at all + assertThat(context.getIdeRoot().resolve(NEW_PROJECT_NAME)).doesNotExist(); assertThat(context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS).resolve(NEW_PROJECT_NAME)).doesNotExist(); } + @Test + void testCreateWithCodeSettingsRepository() { + + // arrange - a combined code and settings repository has the settings in a top-level "settings" folder + WindowsSymlinkTestHelper.assumeSymlinksSupported(); + GitContextImplMock gitContextImplMock = new GitContextImplMock(context, TEST_RESOURCES.resolve("code-settings")); + context.setGitContext(gitContextImplMock); + CreateCommandlet cc = context.getCommandletManager().getCommandlet(CreateCommandlet.class); + cc.newProject.setValueAsString(NEW_PROJECT_NAME, context); + cc.settingsRepo.setValue("https://github.com/devonfw/code-settings-repo.git"); + cc.skipTools.setValue(true); + + // act + cc.run(); + + // assert - the repository is placed into the workspace and IDE_HOME/settings is a symlink to its settings folder + Path newProjectPath = context.getIdeRoot().resolve(NEW_PROJECT_NAME); + Path codePath = newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN).resolve("code-settings-repo"); + assertThat(codePath.resolve("pom.xml")).exists(); + assertThat(codePath.resolve(IdeContext.FOLDER_SETTINGS).resolve("ide.properties")).exists(); + Path settingsLink = newProjectPath.resolve(IdeContext.FOLDER_SETTINGS); + assertThat(settingsLink).isSymbolicLink(); + assertThat(settingsLink.resolve("ide.properties")).exists(); + } + + @Test + void testCreateWithInvalidRepositoryContinuesInForceMode() { + + // arrange - force mode lets the user decide to continue even though the health check failed + GitContextImplMock gitContextImplMock = new GitContextImplMock(context, TEST_RESOURCES.resolve("pypi")); + context.setGitContext(gitContextImplMock); + context.getStartContext().setForceMode(true); + context.setAnswers("yes"); + CreateCommandlet cc = context.getCommandletManager().getCommandlet(CreateCommandlet.class); + cc.newProject.setValueAsString(NEW_PROJECT_NAME, context); + cc.settingsRepo.setValue(IdeContext.DEFAULT_SETTINGS_REPO_URL); + cc.skipTools.setValue(true); + cc.skipRepositories.setValue(true); + + // act + cc.run(); + + // assert + Path newProjectPath = context.getIdeRoot().resolve(NEW_PROJECT_NAME); + assertThat(newProjectPath).exists(); + assertThat(context).logAtWarning() + .hasMessageContaining("does not point to a valid settings or code-settings repository"); + } + @Test void testCreateWithDashPlaceholderAsCliArgument() { // arrange - see https://github.com/devonfw/IDEasy/issues/2106 diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java index cb23034a64..8715b74b4e 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java @@ -13,6 +13,8 @@ import com.devonfw.tools.ide.context.IdeTestContext; import com.devonfw.tools.ide.environment.EnvironmentVariables; import com.devonfw.tools.ide.environment.EnvironmentVariablesType; +import com.devonfw.tools.ide.git.GitContext; +import com.devonfw.tools.ide.git.GitContextMock; import com.devonfw.tools.ide.tool.java.Java; import com.devonfw.tools.ide.tool.mvn.Mvn; import com.devonfw.tools.ide.variable.IdeVariables; @@ -156,4 +158,58 @@ void testRunUpdateSoftwareDoesNotFailWhenSettingsPathIsDeleted(WireMockRuntimeIn assertThat(context).logAtSuccess().hasMessage(SUCCESS_UPDATE_SETTINGS); assertThat(context).logAtSuccess().hasMessageContaining(SUCCESS_INSTALL_OR_UPDATE_SOFTWARE); } + + /** + * Tests that a settings folder that exists but is not a git repository is backed up and cloned from scratch after the user confirmed. + */ + @Test + void testRunUpdateWithBrokenSettingsFolder() { + + // arrange + IdeTestContext context = newContext(PROJECT_UPDATE); + Path settingsPath = context.getSettingsPath(); + // remove the '.git' folder so the settings are present but broken + context.getFileAccess().delete(settingsPath.resolve(GitContext.GIT_FOLDER)); + UpdateCommandlet update = context.getCommandletManager().getCommandlet(UpdateCommandlet.class); + // first answer confirms the backup of the broken settings, second answer picks the default settings repository + context.setAnswers("yes", "-"); + + // act + update.run(); + + // assert + assertThat(context).logAtSuccess().hasMessage(SUCCESS_UPDATE_SETTINGS); + assertThat(context).logAtInfo().hasMessageContaining("Creating backup by moving " + settingsPath); + assertThat(context.getIdeHome().resolve(IdeContext.FOLDER_BACKUPS)).exists(); + assertThat(settingsPath.resolve(GitContext.GIT_FOLDER)).exists(); + assertThat(context).logAtSuccess().hasMessageContaining(SUCCESS_INSTALL_OR_UPDATE_SOFTWARE); + } + + /** + * Tests that a failing "git pull" (e.g. due to an error of a custom git server) only fails the settings step while the software is still installed. + *

+ * See: #2335 for reference. + */ + @Test + void testRunUpdateContinuesWhenPullFails() { + + // arrange + IdeTestContext context = newContext(PROJECT_UPDATE); + context.setGitContext(new GitContextMock(context) { + @Override + public void pull(Path repository) { + + throw new IllegalStateException("git pull failed due to an error of the custom git server"); + } + }); + UpdateCommandlet update = context.getCommandletManager().getCommandlet(UpdateCommandlet.class); + + // act + update.run(); + + // assert + assertThat(context).logAtError().hasMessage("Step 'Applying update' ended with failure."); + assertThat(context).log().hasNoMessage(SUCCESS_UPDATE_SETTINGS); + assertThat(context).logAtSuccess().hasMessageContaining(SUCCESS_INSTALL_OR_UPDATE_SOFTWARE); + } } From 0e1433e9ca02a4705ab0ff024ac40b1d0919377b Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 23:00:31 +0200 Subject: [PATCH 65/89] #1695: updated SettingsUpdater to use to stage system of verify&apply (WIP) --- .../commandlet/update/SettingsUpdater.java | 350 ++++++++++++------ 1 file changed, 238 insertions(+), 112 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java index 14b7b6edff..c64c7de9e4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java @@ -6,8 +6,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.devonfw.tools.ide.cli.CliAbortException; +import com.devonfw.tools.ide.cli.CliException; import com.devonfw.tools.ide.cli.CliRethrowException; -import com.devonfw.tools.ide.context.AbstractIdeContext; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.git.GitContext; import com.devonfw.tools.ide.git.GitUrl; @@ -17,201 +18,326 @@ import com.devonfw.tools.ide.property.StringProperty; /** - * Handles updating/cloning of the settings repository. Returns a result indicating the outcome of the settings update operation. + * Handles the settings repository of the current project in two phases: + *

    + *
  1. {@link #checkSettings() health check}: the settings are always cloned into a temporary directory first where it is verified that the git URL is valid, + * that cloning succeeded, and that the repository actually is a settings or a combined code and settings repository.
  2. + *
  3. {@link #applySettings(SettingsUpdateResult) apply}: only after the health check succeeded the settings are either pulled in place (if they were already + * present) or the verified clone is moved to its final location.
  4. + *
*/ public class SettingsUpdater { private static final Logger LOG = LoggerFactory.getLogger(SettingsUpdater.class); - private final AbstractIdeContext context; - private final StringProperty settingsRepoProperty; - private static final String MESSAGE_SETTINGS_REPO_URL = """ No settings found at {} and no SETTINGS_URL is defined. Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc Please contact the technical lead of your project to get the SETTINGS_URL for your project to enter. In case you just want to test IDEasy you may simply hit return to install the default settings."""; + private static final String MESSAGE_INVALID_REPOSITORY = "Settings repository integrity check failed: " + + "The given git repository URL does not point to a valid settings or code-settings repository. Please verify and try again."; + + private final IdeContext context; + + private final StringProperty settingsRepoProperty; + + /** The temporary directory holding the verified clone or {@code null} if there is nothing to move. */ + private Path tempDir; + + /** The name of the git project - required to place a combined code and settings repository into the workspace. */ + private String gitProjectName; + /** - * Result of the settings update operation. + * Status of the settings {@link SettingsUpdater#checkSettings() health check} describing what {@link SettingsUpdater#applySettings(SettingsUpdateResult)} has + * to do. */ public enum ResultStatus { - /** Settings repository was updated via pull and is valid. */ + /** The settings repository was already present and is valid - it only has to be pulled in place. */ SETTINGS_UPDATED, - /** Settings repository was cloned from scratch (blank state). */ + /** The settings repository was cloned to a temporary directory and is valid - it has to be moved to its final location. */ SETTINGS_CLONED, - /** Settings update failed (could not clone or invalid repository). */ + /** The settings could not be updated but the settings already present are still valid so the process can continue without updating them. */ SETTINGS_UPDATE_FAILED } /** - * Result object containing the outcome and repository type. + * Result of the settings {@link SettingsUpdater#checkSettings() health check}. + * + * @param status the {@link ResultStatus}. + * @param repositoryType the {@link RepositoryType} of the settings repository. + * @param errorMessage the reason why the settings could not be updated or {@code null} if the health check succeeded. */ - public record SettingsUpdateResult(ResultStatus status, RepositoryType repositoryType) { + public record SettingsUpdateResult(ResultStatus status, RepositoryType repositoryType, String errorMessage) { + + /** + * @param status the {@link ResultStatus}. + * @param repositoryType the {@link RepositoryType}. + * @return a {@link SettingsUpdateResult} for a successful health check. + */ + static SettingsUpdateResult of(ResultStatus status, RepositoryType repositoryType) { + + return new SettingsUpdateResult(status, repositoryType, null); + } + + /** + * @param repositoryType the {@link RepositoryType} of the settings that are already present. + * @param errorMessage the reason why the settings could not be updated. + * @return a {@link SettingsUpdateResult} for a failed but recoverable health check. + */ + static SettingsUpdateResult failed(RepositoryType repositoryType, String errorMessage) { + return new SettingsUpdateResult(ResultStatus.SETTINGS_UPDATE_FAILED, repositoryType, errorMessage); + } } /** - * Creates a new SettingsUpdater. + * The constructor. * - * @param context the IDE context - * @param settingsRepoProperty the settings repository property from the update commandlet + * @param context the {@link IdeContext}. + * @param settingsRepoProperty the {@link StringProperty} with the settings repository URL from the update commandlet. */ - public SettingsUpdater(AbstractIdeContext context, StringProperty settingsRepoProperty) { + public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty) { + + super(); this.context = context; this.settingsRepoProperty = settingsRepoProperty; } /** - * Updates the settings repository by either pulling (if exists) or cloning (if new). + * Performs the health check on the settings repository. Nothing is changed in {@link IdeContext#getIdeHome() IDE_HOME} except that a broken settings folder + * is backed up. Whether the settings are pulled or cloned is decided solely by the state of {@link IdeContext#getSettingsPath() IDE_HOME/settings} so that + * {@code ide create} and {@code ide update} share the very same logic. * - * @param codeRepository whether this is a code repository (skip pull if true and not forced) - * @return the result of the settings update operation + * @return the {@link SettingsUpdateResult}. */ - public SettingsUpdateResult updateSettings(boolean codeRepository) { + public SettingsUpdateResult checkSettings() { Path settingsPath = this.context.getSettingsPath(); - boolean isSettingsRepo = this.context.getGitContext().isGitRepo(settingsPath); - - // If it's a code repository and not forced, skip the pull - if (codeRepository && isSettingsRepo && !this.context.isForceMode()) { - LOG.info("Skipping git pull in settings due to code repository. Use --force-pull to enforce pulling."); - return new SettingsUpdateResult(ResultStatus.SETTINGS_UPDATED, RepositoryType.SETTINGS); + if (settingsPath != null) { + // for a combined code and settings repository IDE_HOME/settings is a symlink into the code repository whose '.git' folder is one level above, + // so isGitRepo would report it as broken settings + boolean codeRepository = this.context.isSettingsCodeRepository(); + if (codeRepository || this.context.getGitContext().isGitRepo(settingsPath)) { + return checkPresentSettings(settingsPath, codeRepository ? RepositoryType.CODE_SETTINGS_COMBINED : RepositoryType.SETTINGS); + } } + return checkClonedSettings(settingsPath); + } - if (isSettingsRepo) { - // Existing settings repository - pull updates - return pullExistingSettings(settingsPath); - } else { - // No existing settings - clone from scratch - return cloneSettings(); + /** + * Applies the result of the {@link #checkSettings() health check} by either pulling the settings in place or moving the verified clone to its final + * location. + * + * @param result the {@link SettingsUpdateResult} from {@link #checkSettings()}. + */ + public void applySettings(SettingsUpdateResult result) { + + switch (result.status()) { + case SETTINGS_UPDATED -> pullSettings(); + case SETTINGS_CLONED -> moveSettings(result.repositoryType()); + case SETTINGS_UPDATE_FAILED -> LOG.error("Settings repository has not been updated: {}", result.errorMessage()); } } - private SettingsUpdateResult pullExistingSettings(Path settingsPath) { + /** + * Health check for settings that are already present. As the project keeps working with these settings, a failure is only fatal if the user explicitly + * aborted. + */ + private SettingsUpdateResult checkPresentSettings(Path settingsPath, RepositoryType repositoryType) { - GitContext gitContext = this.context.getGitContext(); - if (gitContext.hasUntrackedFiles(settingsPath)) { - gitContext.pullSafelyWithStash(settingsPath); - } else { - gitContext.pull(settingsPath); + try { + GitUrl gitUrl = GitUrl.of(this.context.getGitContext().retrieveGitUrl(settingsPath)); + RepositoryType clonedType = RepositoryUtil.getRepositoryType(cloneToTempDir(gitUrl)); + deleteTempDir(); + if (!isSettingsRepository(clonedType) && !confirmInvalidRepository(clonedType, gitUrl)) { + return SettingsUpdateResult.failed(repositoryType, MESSAGE_INVALID_REPOSITORY); + } + return SettingsUpdateResult.of(ResultStatus.SETTINGS_UPDATED, repositoryType); + } catch (RuntimeException e) { + deleteTempDir(); + if (e instanceof CliAbortException) { + // the user answered "no" so we must not silently carry on + throw toFatalException(e); + } + return SettingsUpdateResult.failed(repositoryType, e.getMessage()); } - this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); - return new SettingsUpdateResult(ResultStatus.SETTINGS_UPDATED, RepositoryType.SETTINGS); } - private SettingsUpdateResult cloneSettings() { + /** + * Health check for missing or broken settings. Without valid settings there is nothing to continue with, so every failure is fatal here. + */ + private SettingsUpdateResult checkClonedSettings(Path settingsPath) { - Path tempProjectPath = null; try { - // Get settings URL + backupBrokenSettings(settingsPath); GitUrl gitUrl = getOrAskSettingsUrl(); - - // Use unique temp directory to avoid leftovers from previous attempts - tempProjectPath = createUniqueTempProjectPath(); - this.context.getGitContext().pullOrClone(gitUrl, tempProjectPath); - return checkIntegrityAndMove(tempProjectPath, gitUrl.getProjectName()); - } catch (Exception e) { - // Clean up temp directory on failure - if (tempProjectPath != null) { - this.context.getFileAccess().backup(tempProjectPath); + RepositoryType repositoryType = RepositoryUtil.getRepositoryType(cloneToTempDir(gitUrl)); + if (!isSettingsRepository(repositoryType) && !confirmInvalidRepository(repositoryType, gitUrl)) { + throw new CliRethrowException(MESSAGE_INVALID_REPOSITORY); } - throw new CliRethrowException("Settings repository integrity check failed: " + e.getMessage(), e); + return SettingsUpdateResult.of(ResultStatus.SETTINGS_CLONED, repositoryType); + } catch (RuntimeException e) { + deleteTempDir(); + throw toFatalException(e); } } - private GitUrl getOrAskSettingsUrl() { + /** + * @param error the {@link RuntimeException} that made the settings setup fail. + * @return a {@link CliRethrowException} that aborts the entire process. An existing {@link CliException} keeps its message and + * {@link CliException#getExitCode() exit code} so that e.g. an abort by the user is still reported as such. + */ + private static CliRethrowException toFatalException(RuntimeException error) { - String repository = this.settingsRepoProperty.getValue(); - repository = handleDefaultRepository(repository); - String userPrompt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; - String defaultUrl = IdeContext.DEFAULT_SETTINGS_REPO_URL; - LOG.info(MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath()); + if (error instanceof CliRethrowException rethrow) { + return rethrow; + } else if (error instanceof CliException) { + return new CliRethrowException(error.getMessage(), error); + } + return new CliRethrowException("Failed to set up the settings repository: " + error.getMessage(), error); + } - GitUrl gitUrl = null; - if (repository != null) { - gitUrl = GitUrl.of(repository); + private void pullSettings() { + + Path settingsPath = this.context.getSettingsPath(); + GitContext gitContext = this.context.getGitContext(); + if (gitContext.hasUntrackedFiles(settingsPath)) { + gitContext.pullSafelyWithStash(settingsPath); + } else { + gitContext.pull(settingsPath); } - while ((gitUrl == null) || !gitUrl.isValid()) { - repository = this.context.askForInput(userPrompt, defaultUrl); - repository = handleDefaultRepository(repository); - gitUrl = GitUrl.of(repository); - if (!gitUrl.isValid()) { - LOG.warn("The input URL is not valid, please try again."); + gitContext.saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); + } + + private void moveSettings(RepositoryType repositoryType) { + + Path settingsPath = this.context.getSettingsPath(); + if ((repositoryType == RepositoryType.SETTINGS) || (repositoryType == RepositoryType.UNKNOWN)) { + moveProject(this.tempDir, settingsPath); + this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); + } else { + // for a code repository we clone into the workspace and symlink IDE_HOME/settings to its settings folder + Path codePath = this.context.getWorkspacePath().resolve(this.gitProjectName); + moveProject(this.tempDir, codePath); + Path settingsFolder = codePath.resolve(IdeContext.FOLDER_SETTINGS); + if (Files.isDirectory(settingsFolder)) { + this.context.getFileAccess().symlink(settingsFolder, settingsPath); + this.context.getGitContext().saveCurrentCommitId(settingsFolder, this.context.getSettingsCommitIdPath()); + } else { + LOG.warn("The repository has been cloned to {} but it does not contain a settings folder so your project has no settings.", codePath); } } - return gitUrl; + this.tempDir = null; } - private Path createUniqueTempProjectPath() { + private Path cloneToTempDir(GitUrl gitUrl) { - // Use FileAccess.createTempDir to ensure unique directory and avoid leftovers - FileAccess fileAccess = this.context.getFileAccess(); - Path tempProjectsDir = this.context.getTempPath().resolve(IdeContext.FOLDER_PROJECTS); - fileAccess.mkdirs(tempProjectsDir); - return fileAccess.createTempDir(this.context.getProjectName() + "-"); + this.gitProjectName = gitUrl.getProjectName(); + // createTempDir guarantees a unique and empty directory so no leftovers of a previous attempt can interfere and we can clone directly + this.tempDir = this.context.getFileAccess().createTempDir(this.gitProjectName + "-"); + this.context.getGitContext().clone(gitUrl, this.tempDir); + return this.tempDir; } - private SettingsUpdateResult checkIntegrityAndMove(Path projectPath, String gitProjectName) { + private void backupBrokenSettings(Path settingsPath) { + if ((settingsPath == null) || !Files.exists(settingsPath)) { + return; + } FileAccess fileAccess = this.context.getFileAccess(); + if (!fileAccess.isEmptyDir(settingsPath)) { + this.context.askToContinue(""" + Your settings repository seems to be broken ('.git' folder not present). + We can fix this by moving your settings to the backup. + You will be asked for the settings git URL and your settings will be cloned from scratch. + Do you want to proceed?"""); + } + fileAccess.backup(settingsPath); + } - if (!Files.exists(projectPath)) { - throw new CliRethrowException("Git pull target folder does not exist."); + /** + * @return {@code true} if the user explicitly wants to continue with an invalid repository, {@code false} otherwise. + */ + private boolean confirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl) { + + if (!this.context.isForceMode()) { + return false; } + LOG.warn("{}\nURL: {}\nDetected repository type: {}", MESSAGE_INVALID_REPOSITORY, gitUrl, repositoryType); + this.context.askToContinue("Force mode is active. Do you want to continue anyway?"); + return true; + } - Path targetDirectory; - RepositoryType repoType = RepositoryUtil.getRepositoryType(projectPath, gitProjectName); + private static boolean isSettingsRepository(RepositoryType repositoryType) { - switch (repoType) { - case SETTINGS -> { - targetDirectory = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); - moveProject(projectPath, targetDirectory); - this.context.getGitContext().saveCurrentCommitId(targetDirectory, this.context.getSettingsCommitIdPath()); - return new SettingsUpdateResult(ResultStatus.SETTINGS_CLONED, RepositoryType.SETTINGS); - } - case CODE_SETTINGS_COMBINED -> { - // Special case: symlink from IDE_HOME/settings to workspace/repo_name/settings - targetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); - moveProject(projectPath, targetDirectory); + return (repositoryType == RepositoryType.SETTINGS) || (repositoryType == RepositoryType.CODE_SETTINGS_COMBINED); + } - Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); - Path symlinkTargetPath = this.context.getWorkspacePath().resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS); + /** + * Releases the temporary clone if it has not been moved to its final location. The clone is created by {@link #checkSettings()} and consumed by + * {@link #applySettings(SettingsUpdateResult)}, so its lifetime spans both phases and has to be ended by the caller once it is done with them. + */ + public void cleanup() { - fileAccess.symlink(symlinkTargetPath, symlinkPath); - this.context.getGitContext().saveCurrentCommitId(symlinkTargetPath, this.context.getSettingsCommitIdPath()); - return new SettingsUpdateResult(ResultStatus.SETTINGS_CLONED, RepositoryType.CODE_SETTINGS_COMBINED); - } - default -> { - fileAccess.backup(projectPath); - throw new CliRethrowException(getIntegrityCheckErrorMessage(String.format( - "The given git repository URL does not point to a valid settings or code-settings repository. " - + "Please verify and try again. Before trying again, please delete the folder %s", - this.context.getIdeHome()))); - } - } + deleteTempDir(); } - private Path moveProject(Path from, Path to) { + /** + * Removes the temporary clone. It is deleted and not backed up since it only contains a fresh clone without any user data and a backup would be created + * inside {@link IdeContext#getIdeHome() IDE_HOME} that may not even exist yet. Failures are only logged so that the actual error never gets masked. + */ + private void deleteTempDir() { - FileAccess fileAccess = this.context.getFileAccess(); + if (this.tempDir == null) { + return; + } try { - fileAccess.move(from, to); - } catch (Exception e) { - throw new CliRethrowException(String.format("Failed to move project from %s to %s", from, to), e); + this.context.getFileAccess().delete(this.tempDir); + } catch (RuntimeException e) { + LOG.warn("Failed to delete temporary directory {}", this.tempDir, e); } - return to; + this.tempDir = null; } - private String getIntegrityCheckErrorMessage(String message) { - return String.format("Settings repository integrity check failed: %s", message); + private GitUrl getOrAskSettingsUrl() { + + String repository = handleDefaultRepository(this.settingsRepoProperty.getValue()); + GitUrl gitUrl = null; + if (repository != null) { + gitUrl = GitUrl.of(repository); + } + if ((gitUrl == null) || !gitUrl.isValid()) { + LOG.info(MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath()); + } + String userPrompt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; + while ((gitUrl == null) || !gitUrl.isValid()) { + repository = handleDefaultRepository(this.context.askForInput(userPrompt, IdeContext.DEFAULT_SETTINGS_REPO_URL)); + gitUrl = GitUrl.of(repository); + if (!gitUrl.isValid()) { + LOG.warn("The input URL is not valid, please try again."); + } + } + return gitUrl; } private String handleDefaultRepository(String repository) { + if ("-".equals(repository)) { 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 moveProject(Path from, Path to) { + + try { + this.context.getFileAccess().move(from, to); + } catch (RuntimeException e) { + // FileAccess already reports source, target and the Windows file-lock hint so we only escalate to a fatal error here + throw new CliRethrowException(e.getMessage(), e); + } + } } From 6311e682542490afec7ba6c3ac4a2448a9c6e40d Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 23:06:53 +0200 Subject: [PATCH 66/89] #1695: cleanup --- .../update/AbstractUpdateCommandlet.java | 8 ++++---- .../ide/commandlet/update/SettingsUpdater.java | 17 ++++------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 725254c3df..ed914ece7b 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -174,24 +174,24 @@ protected void updateSettings() { protected String getStepMessage() { - return "update (pull) settings repository"; + return "Update settings repository"; } private void updateSettingsInStep(Step step) { SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo); try { - SettingsUpdateResult result = this.context.newStep("Performing health check on settings").call(settingsUpdater::checkSettings, () -> null); + SettingsUpdateResult result = this.context.newStep("Performing settings health check").call(settingsUpdater::checkSettings, () -> null); // fatal problems (e.g. no valid settings at all) were already rethrown, so reaching this point means the settings we have stay usable if (result == null) { - step.error("Health check on settings failed - the settings have not been updated."); + step.error("Health check on settings failed due to unknown error - the settings have not been updated."); return; } else if (result.status() == ResultStatus.SETTINGS_UPDATE_FAILED) { step.error("The settings have not been updated: {}", result.errorMessage()); return; } prepareProject(); - boolean applied = this.context.newStep("Applying update").run(() -> settingsUpdater.applySettings(result)); + boolean applied = this.context.newStep("Applying settings").run(() -> settingsUpdater.applySettings(result)); if (!applied) { step.error("Failed to apply the settings update."); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java index c64c7de9e4..24a883d6be 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java @@ -150,13 +150,13 @@ private SettingsUpdateResult checkPresentSettings(Path settingsPath, RepositoryT try { GitUrl gitUrl = GitUrl.of(this.context.getGitContext().retrieveGitUrl(settingsPath)); RepositoryType clonedType = RepositoryUtil.getRepositoryType(cloneToTempDir(gitUrl)); - deleteTempDir(); + cleanup(); if (!isSettingsRepository(clonedType) && !confirmInvalidRepository(clonedType, gitUrl)) { return SettingsUpdateResult.failed(repositoryType, MESSAGE_INVALID_REPOSITORY); } return SettingsUpdateResult.of(ResultStatus.SETTINGS_UPDATED, repositoryType); } catch (RuntimeException e) { - deleteTempDir(); + cleanup(); if (e instanceof CliAbortException) { // the user answered "no" so we must not silently carry on throw toFatalException(e); @@ -179,7 +179,7 @@ private SettingsUpdateResult checkClonedSettings(Path settingsPath) { } return SettingsUpdateResult.of(ResultStatus.SETTINGS_CLONED, repositoryType); } catch (RuntimeException e) { - deleteTempDir(); + cleanup(); throw toFatalException(e); } } @@ -275,20 +275,11 @@ private static boolean isSettingsRepository(RepositoryType repositoryType) { return (repositoryType == RepositoryType.SETTINGS) || (repositoryType == RepositoryType.CODE_SETTINGS_COMBINED); } - /** - * Releases the temporary clone if it has not been moved to its final location. The clone is created by {@link #checkSettings()} and consumed by - * {@link #applySettings(SettingsUpdateResult)}, so its lifetime spans both phases and has to be ended by the caller once it is done with them. - */ - public void cleanup() { - - deleteTempDir(); - } - /** * Removes the temporary clone. It is deleted and not backed up since it only contains a fresh clone without any user data and a backup would be created * inside {@link IdeContext#getIdeHome() IDE_HOME} that may not even exist yet. Failures are only logged so that the actual error never gets masked. */ - private void deleteTempDir() { + public void cleanup() { if (this.tempDir == null) { return; From c67aabeddc883b9a37633370687cd79dd8b1c57b Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 26 Aug 2026 23:24:39 +0200 Subject: [PATCH 67/89] #1695: spotless apply --- .../tools/ide/commandlet/update/AbstractUpdateCommandlet.java | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index ed914ece7b..36f37d10e7 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -182,6 +182,7 @@ private void updateSettingsInStep(Step step) { SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo); try { SettingsUpdateResult result = this.context.newStep("Performing settings health check").call(settingsUpdater::checkSettings, () -> null); + // fatal problems (e.g. no valid settings at all) were already rethrown, so reaching this point means the settings we have stay usable if (result == null) { step.error("Health check on settings failed due to unknown error - the settings have not been updated."); From 4deda90004cdee35460dc8c4a079a66b05a78f41 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Thu, 27 Aug 2026 14:03:29 +0200 Subject: [PATCH 68/89] #1695: finalized verification workflow when no settings repo exists yet; update workflow WIP --- ...wException.java => CliFatalException.java} | 6 +- .../ide/commandlet/CreateCommandlet.java | 2 +- .../ide/commandlet/StatusCommandlet.java | 2 +- .../update/AbstractUpdateCommandlet.java | 60 ++++-- .../settings/HealthCheckResultStatus.java | 14 ++ .../settings/SettingsHealthCheckResult.java | 35 ++++ .../update/settings/SettingsUpdateResult.java | 7 + .../update/settings/SettingsUpdateStatus.java | 11 + .../{ => settings}/SettingsUpdater.java | 192 +++++++++--------- .../tools/ide/context/AbstractIdeContext.java | 6 +- .../devonfw/tools/ide/context/IdeContext.java | 2 +- .../com/devonfw/tools/ide/step/StepTest.java | 10 +- 12 files changed, 221 insertions(+), 126 deletions(-) rename cli/src/main/java/com/devonfw/tools/ide/cli/{CliRethrowException.java => CliFatalException.java} (83%) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateResult.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateStatus.java rename cli/src/main/java/com/devonfw/tools/ide/commandlet/update/{ => settings}/SettingsUpdater.java (61%) diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliFatalException.java similarity index 83% rename from cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java rename to cli/src/main/java/com/devonfw/tools/ide/cli/CliFatalException.java index fc991233bd..11a153aa76 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/cli/CliRethrowException.java +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliFatalException.java @@ -5,14 +5,14 @@ * settings repository). Unlike a regular error that only makes the current {@link com.devonfw.tools.ide.step.Step Step} fail while the overall process * continues, this exception {@link #isForceRethrowInStep() is always re-thrown} so no further step is executed in an invalid state. */ -public final class CliRethrowException extends CliException { +public final class CliFatalException extends CliException { /** * The constructor. * * @param message the {@link #getMessage() message}. */ - public CliRethrowException(String message) { + public CliFatalException(String message) { super(message); } @@ -23,7 +23,7 @@ public CliRethrowException(String message) { * @param message the {@link #getMessage() message}. * @param cause the {@link #getCause() cause}. */ - public CliRethrowException(String message, Throwable cause) { + public CliFatalException(String message, Throwable cause) { super(message, cause); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index 67e2256983..e5c5b24b82 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -68,7 +68,7 @@ protected void doRun() { } @Override - protected void prepareProject() { + protected void onSettingHealthCheckSucceeded() { // only called after the settings passed the health check Path newProjectPath = getNewProjectPath(); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java index 39a5131ff5..1cd9fe2fcd 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java @@ -104,7 +104,7 @@ private void logSettingsGitStatus() { } else { GitContext gitContext = this.context.getGitContext(); if (gitContext.isRepositoryUpdateAvailable(settingsPath, this.context.getSettingsCommitIdPath())) { - if (!this.context.isSettingsCodeRepository()) { + if (!this.context.isCombinedSettingsCodeRepository()) { LOG.warn("Your settings are not up-to-date, please run 'ide update'."); } } else { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 36f37d10e7..5019e54430 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -9,14 +9,17 @@ import java.util.Set; import java.util.stream.Stream; +import com.devonfw.tools.ide.commandlet.update.settings.HealthCheckResultStatus; +import com.devonfw.tools.ide.commandlet.update.settings.SettingsHealthCheckResult; +import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdateResult; +import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdater; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.devonfw.tools.ide.commandlet.Commandlet; import com.devonfw.tools.ide.commandlet.CommandletManager; import com.devonfw.tools.ide.commandlet.CreateCommandlet; -import com.devonfw.tools.ide.commandlet.update.SettingsUpdater.ResultStatus; -import com.devonfw.tools.ide.commandlet.update.SettingsUpdater.SettingsUpdateResult; import com.devonfw.tools.ide.context.AbstractIdeContext; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.context.IdeStartContextImpl; @@ -102,7 +105,7 @@ protected void doRun() { * Hook that is called after the settings passed the health check but before they are moved to their final location. Does nothing by default and is overridden * by {@link CreateCommandlet} to create the project structure so that no project is created at all if the health check failed. */ - protected void prepareProject() { + protected void onSettingHealthCheckSucceeded() { // nothing to do by default } @@ -163,7 +166,7 @@ private void setupConf(Path template, Path conf) { */ protected void updateSettings() { - boolean codeRepository = this.context.isSettingsCodeRepository(); + boolean codeRepository = this.context.isCombinedSettingsCodeRepository(); if (codeRepository && !(this.context.isForceMode() || this.forcePull.isTrue())) { LOG.info("Skipping git pull in settings due to code repository. Use --force-pull to enforce pulling."); return; @@ -181,21 +184,42 @@ private void updateSettingsInStep(Step step) { SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo); try { - SettingsUpdateResult result = this.context.newStep("Performing settings health check").call(settingsUpdater::checkSettings, () -> null); + //Step 1: Perform health check + Step healthCheckStep = this.context.newStep("Performing settings health check"); + Path temporaryRepoDir = healthCheckStep.call(() -> { + SettingsHealthCheckResult healthCheckResult = settingsUpdater.checkSettings(this.context.getSettingsPath()); + HealthCheckResultStatus status = healthCheckResult.status(); + + if (status == null) { + healthCheckStep.error("Health check on settings failed due to unknown error - the settings have not been updated."); + return healthCheckResult.temporarySettingsDirectory(); + } else if (healthCheckResult.status() == HealthCheckResultStatus.SETTINGS_INVALID) { + healthCheckStep.error("The settings have not been updated: {}", healthCheckResult.errorMessage()); + return healthCheckResult.temporarySettingsDirectory(); + } + return healthCheckResult.temporarySettingsDirectory(); + }, () -> null); + if(temporaryRepoDir == null || healthCheckStep.isFailure()) return; - // fatal problems (e.g. no valid settings at all) were already rethrown, so reaching this point means the settings we have stay usable - if (result == null) { - step.error("Health check on settings failed due to unknown error - the settings have not been updated."); - return; - } else if (result.status() == ResultStatus.SETTINGS_UPDATE_FAILED) { - step.error("The settings have not been updated: {}", result.errorMessage()); - return; - } - prepareProject(); - boolean applied = this.context.newStep("Applying settings").run(() -> settingsUpdater.applySettings(result)); - if (!applied) { - step.error("Failed to apply the settings update."); - } + //Step 2: Let create/update commandlets prepare themselves for the settings update. + onSettingHealthCheckSucceeded(); + + //Step 3: Apply (move/pull newest version) settings + Step applySettingsStep = this.context.newStep("Applying settings"); + applySettingsStep.run(() -> { + SettingsUpdateResult settingsUpdateResult = settingsUpdater.applySettings(temporaryRepoDir); + + if (settingsUpdateResult == null) { + applySettingsStep.error("Failed to apply the settings update due to unknown error."); + return; + } + switch (settingsUpdateResult.updateStatus()) { + case SETTINGS_UPDATED -> applySettingsStep.success("Settings update successfully applied"); + case SETTINGS_CLONED -> applySettingsStep.success("Settings successfully applied (cloned)"); + case SETTINGS_UPDATE_FAILED -> applySettingsStep.error("The settings update could not be applied: {}", settingsUpdateResult.errorMessage()); + case null, default -> applySettingsStep.error("Unexpected value: {}", settingsUpdateResult.updateStatus()); + } + }); } finally { // the verified clone lives across both steps and the prepareProject hook so it is only here that its lifetime ends settingsUpdater.cleanup(); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java new file mode 100644 index 0000000000..673ef4f00b --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java @@ -0,0 +1,14 @@ +package com.devonfw.tools.ide.commandlet.update.settings; + +/** + * Status of the settings {@link SettingsUpdater#checkSettings() health check} describing what {@link SettingsUpdater#applySettings(SettingsHealthCheckResult)} has + * to do. + */ +public enum HealthCheckResultStatus { + /** The settings repository was cloned to a temporary directory and is valid - it can be moved to its final location. */ + SETTINGS_VALID, + /** The settings repository already existed and was cloned to a temporary directory and is valid - it can be moved to its final location. */ + SETTINGS_VALID_EXISTING, + /** The settings repository is invalid */ + SETTINGS_INVALID +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java new file mode 100644 index 0000000000..e28692c112 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java @@ -0,0 +1,35 @@ +package com.devonfw.tools.ide.commandlet.update.settings; + +import com.devonfw.tools.ide.git.repository.RepositoryType; + +import java.nio.file.Path; + +/** + * Result of the settings {@link SettingsUpdater#checkSettings() health check}. + * + * @param status the {@link HealthCheckResultStatus}. + * @param repositoryType the {@link RepositoryType} of the settings repository. + * @param errorMessage the reason why the settings could not be updated or {@code null} if the health check succeeded. + */ +public record SettingsHealthCheckResult(HealthCheckResultStatus status, RepositoryType repositoryType, Path temporarySettingsDirectory, String errorMessage) { + + /** + * @param status the {@link HealthCheckResultStatus}. + * @param repositoryType the {@link RepositoryType}. + * @return a {@link SettingsHealthCheckResult} for a successful health check. + */ + public static SettingsHealthCheckResult of(HealthCheckResultStatus status, RepositoryType repositoryType, Path temporaryRepoDirectory) { + + return new SettingsHealthCheckResult(status, repositoryType, temporaryRepoDirectory, null); + } + + /** + * @param repositoryType the {@link RepositoryType} of the settings that are already present. + * @param errorMessage the reason why the settings could not be updated. + * @return a {@link SettingsHealthCheckResult} for a failed but recoverable health check. + */ + public static SettingsHealthCheckResult failed(RepositoryType repositoryType, String errorMessage, Path temporaryRepoDirectory) { + + return new SettingsHealthCheckResult(HealthCheckResultStatus.SETTINGS_INVALID, repositoryType, temporaryRepoDirectory, errorMessage); + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateResult.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateResult.java new file mode 100644 index 0000000000..27b82b2d4c --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateResult.java @@ -0,0 +1,7 @@ +package com.devonfw.tools.ide.commandlet.update.settings; + +import com.devonfw.tools.ide.git.repository.RepositoryType; + +public record SettingsUpdateResult(SettingsUpdateStatus updateStatus, RepositoryType repositoryType, String errorMessage) { + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateStatus.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateStatus.java new file mode 100644 index 0000000000..d13871f4eb --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateStatus.java @@ -0,0 +1,11 @@ +package com.devonfw.tools.ide.commandlet.update.settings; + +/// Status of the update action of a settings repo. +public enum SettingsUpdateStatus { + /** Existing settings have been successfully updated **/ + SETTINGS_UPDATED, + /** Freshly cloned settings have been successfully applied **/ + SETTINGS_CLONED, + /** Error occurred **/ + SETTINGS_UPDATE_FAILED +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java similarity index 61% rename from cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java rename to cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index 24a883d6be..6df8c24f9a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -1,4 +1,4 @@ -package com.devonfw.tools.ide.commandlet.update; +package com.devonfw.tools.ide.commandlet.update.settings; import java.nio.file.Files; import java.nio.file.Path; @@ -8,7 +8,7 @@ import com.devonfw.tools.ide.cli.CliAbortException; import com.devonfw.tools.ide.cli.CliException; -import com.devonfw.tools.ide.cli.CliRethrowException; +import com.devonfw.tools.ide.cli.CliFatalException; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.git.GitContext; import com.devonfw.tools.ide.git.GitUrl; @@ -22,7 +22,7 @@ *
    *
  1. {@link #checkSettings() health check}: the settings are always cloned into a temporary directory first where it is verified that the git URL is valid, * that cloning succeeded, and that the repository actually is a settings or a combined code and settings repository.
  2. - *
  3. {@link #applySettings(SettingsUpdateResult) apply}: only after the health check succeeded the settings are either pulled in place (if they were already + *
  4. {@link #applySettings(SettingsHealthCheckResult) apply}: only after the health check succeeded the settings are either pulled in place (if they were already * present) or the verified clone is moved to its final location.
  5. *
*/ @@ -41,57 +41,16 @@ public class SettingsUpdater { private final IdeContext context; + private final FileAccess fileAccess; + private final StringProperty settingsRepoProperty; /** The temporary directory holding the verified clone or {@code null} if there is nothing to move. */ - private Path tempDir; + private Path tempRepoDir; /** The name of the git project - required to place a combined code and settings repository into the workspace. */ private String gitProjectName; - /** - * Status of the settings {@link SettingsUpdater#checkSettings() health check} describing what {@link SettingsUpdater#applySettings(SettingsUpdateResult)} has - * to do. - */ - public enum ResultStatus { - /** The settings repository was already present and is valid - it only has to be pulled in place. */ - SETTINGS_UPDATED, - /** The settings repository was cloned to a temporary directory and is valid - it has to be moved to its final location. */ - SETTINGS_CLONED, - /** The settings could not be updated but the settings already present are still valid so the process can continue without updating them. */ - SETTINGS_UPDATE_FAILED - } - - /** - * Result of the settings {@link SettingsUpdater#checkSettings() health check}. - * - * @param status the {@link ResultStatus}. - * @param repositoryType the {@link RepositoryType} of the settings repository. - * @param errorMessage the reason why the settings could not be updated or {@code null} if the health check succeeded. - */ - public record SettingsUpdateResult(ResultStatus status, RepositoryType repositoryType, String errorMessage) { - - /** - * @param status the {@link ResultStatus}. - * @param repositoryType the {@link RepositoryType}. - * @return a {@link SettingsUpdateResult} for a successful health check. - */ - static SettingsUpdateResult of(ResultStatus status, RepositoryType repositoryType) { - - return new SettingsUpdateResult(status, repositoryType, null); - } - - /** - * @param repositoryType the {@link RepositoryType} of the settings that are already present. - * @param errorMessage the reason why the settings could not be updated. - * @return a {@link SettingsUpdateResult} for a failed but recoverable health check. - */ - static SettingsUpdateResult failed(RepositoryType repositoryType, String errorMessage) { - - return new SettingsUpdateResult(ResultStatus.SETTINGS_UPDATE_FAILED, repositoryType, errorMessage); - } - } - /** * The constructor. * @@ -103,6 +62,7 @@ public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty) super(); this.context = context; this.settingsRepoProperty = settingsRepoProperty; + this.fileAccess = context.getFileAccess(); } /** @@ -110,17 +70,16 @@ public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty) * is backed up. Whether the settings are pulled or cloned is decided solely by the state of {@link IdeContext#getSettingsPath() IDE_HOME/settings} so that * {@code ide create} and {@code ide update} share the very same logic. * - * @return the {@link SettingsUpdateResult}. + * @return the {@link SettingsHealthCheckResult}. */ - public SettingsUpdateResult checkSettings() { + public SettingsHealthCheckResult checkSettings(Path settingsPath) { - Path settingsPath = this.context.getSettingsPath(); - if (settingsPath != null) { + if (settingsPath != null && !fileAccess.isEmptyDir(settingsPath)) { // for a combined code and settings repository IDE_HOME/settings is a symlink into the code repository whose '.git' folder is one level above, // so isGitRepo would report it as broken settings - boolean codeRepository = this.context.isSettingsCodeRepository(); - if (codeRepository || this.context.getGitContext().isGitRepo(settingsPath)) { - return checkPresentSettings(settingsPath, codeRepository ? RepositoryType.CODE_SETTINGS_COMBINED : RepositoryType.SETTINGS); + RepositoryType settingsRepoType = RepositoryUtil.getRepositoryType(settingsPath); + if (isSettingsOrCodeSettingsRepository(settingsRepoType)) { + return checkSettingsPresent(settingsPath, settingsRepoType); } } return checkClonedSettings(settingsPath); @@ -130,75 +89,114 @@ public SettingsUpdateResult checkSettings() { * Applies the result of the {@link #checkSettings() health check} by either pulling the settings in place or moving the verified clone to its final * location. * - * @param result the {@link SettingsUpdateResult} from {@link #checkSettings()}. + * @param sourcePath sourcePath of the settings to apply. + * @return a {@link SettingsUpdateResult} representing the state, whether moving/pulling the newest settings was successful. */ - public void applySettings(SettingsUpdateResult result) { + public SettingsUpdateResult applySettings(Path sourcePath) { + + RepositoryType repositoryType = RepositoryUtil.getRepositoryType(sourcePath); - switch (result.status()) { - case SETTINGS_UPDATED -> pullSettings(); - case SETTINGS_CLONED -> moveSettings(result.repositoryType()); - case SETTINGS_UPDATE_FAILED -> LOG.error("Settings repository has not been updated: {}", result.errorMessage()); + switch (repositoryType) { + case CODE -> { + //Technically should be caught during a health check, but we still handle this here. + + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, MESSAGE_INVALID_REPOSITORY); + } + case SETTINGS -> { + //move to IDE_HOME/SETTINGS + + moveProject(sourcePath, context.getSettingsPath()); + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); + } + case CODE_SETTINGS_COMBINED -> { + //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. (Formerly managed by the obsolete "--code" flag) + Path targetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); + moveProject(sourcePath, targetDirectory); + + Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); + Path symlinkTargetPath = this.context.getWorkspacePath().resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS); + + context.getFileAccess().symlink(symlinkTargetPath, symlinkPath); + this.context.getGitContext().saveCurrentCommitId(symlinkTargetPath, this.context.getSettingsCommitIdPath()); + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); + } + case UNKNOWN -> { + + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, MESSAGE_INVALID_REPOSITORY); + } } + + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Unknown error during settings"); } /** * Health check for settings that are already present. As the project keeps working with these settings, a failure is only fatal if the user explicitly - * aborted. + * aborted. Here, if a new version is available, we clone the new version into a temporary folder and perform health checks. If the cloned, new version is valid, + * we call git update in the existing settings folder. */ - private SettingsUpdateResult checkPresentSettings(Path settingsPath, RepositoryType repositoryType) { + private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, RepositoryType repositoryType) { try { + //Get Git url of existing settings, clone newest version of them to temp dir GitUrl gitUrl = GitUrl.of(this.context.getGitContext().retrieveGitUrl(settingsPath)); - RepositoryType clonedType = RepositoryUtil.getRepositoryType(cloneToTempDir(gitUrl)); + RepositoryType clonedType = RepositoryUtil.getRepositoryType(cloneRepoToTempDir(gitUrl)); cleanup(); - if (!isSettingsRepository(clonedType) && !confirmInvalidRepository(clonedType, gitUrl)) { - return SettingsUpdateResult.failed(repositoryType, MESSAGE_INVALID_REPOSITORY); + + //If cloned repo is not (code-)settings repo and no force override (e.g. force mode) is applied, return error. + if (!isSettingsOrCodeSettingsRepository(clonedType) && !requestUserConfirmInvalidRepository(clonedType, gitUrl)) { + return SettingsHealthCheckResult.failed(clonedType, MESSAGE_INVALID_REPOSITORY, settingsPath); } - return SettingsUpdateResult.of(ResultStatus.SETTINGS_UPDATED, repositoryType); + + //Otherwise, (e.g. user overrides), return valid. + return SettingsHealthCheckResult.of(HealthCheckResultStatus.SETTINGS_VALID, repositoryType, settingsPath); } catch (RuntimeException e) { cleanup(); if (e instanceof CliAbortException) { // the user answered "no" so we must not silently carry on - throw toFatalException(e); + throw createGuaranteedFatalException(e); } - return SettingsUpdateResult.failed(repositoryType, e.getMessage()); + return SettingsHealthCheckResult.failed(repositoryType, e.getMessage(), settingsPath); } } /** * Health check for missing or broken settings. Without valid settings there is nothing to continue with, so every failure is fatal here. */ - private SettingsUpdateResult checkClonedSettings(Path settingsPath) { + private SettingsHealthCheckResult checkClonedSettings(Path settingsPath) { try { backupBrokenSettings(settingsPath); GitUrl gitUrl = getOrAskSettingsUrl(); - RepositoryType repositoryType = RepositoryUtil.getRepositoryType(cloneToTempDir(gitUrl)); - if (!isSettingsRepository(repositoryType) && !confirmInvalidRepository(repositoryType, gitUrl)) { - throw new CliRethrowException(MESSAGE_INVALID_REPOSITORY); + + Path tempCloneDir = cloneRepoToTempDir(gitUrl); + RepositoryType repositoryType = RepositoryUtil.getRepositoryType(tempCloneDir); + if (!isSettingsOrCodeSettingsRepository(repositoryType) && !requestUserConfirmInvalidRepository(repositoryType, gitUrl)) { + //see @javadoc why we throw fatally here. + throw new CliFatalException(MESSAGE_INVALID_REPOSITORY); } - return SettingsUpdateResult.of(ResultStatus.SETTINGS_CLONED, repositoryType); + return SettingsHealthCheckResult.of(HealthCheckResultStatus.SETTINGS_VALID, repositoryType, tempCloneDir); } catch (RuntimeException e) { cleanup(); - throw toFatalException(e); + throw createGuaranteedFatalException(e); } } /** * @param error the {@link RuntimeException} that made the settings setup fail. - * @return a {@link CliRethrowException} that aborts the entire process. An existing {@link CliException} keeps its message and + * @return a {@link CliFatalException} that aborts the entire process. An existing {@link CliException} keeps its message and * {@link CliException#getExitCode() exit code} so that e.g. an abort by the user is still reported as such. */ - private static CliRethrowException toFatalException(RuntimeException error) { + private static CliFatalException createGuaranteedFatalException(RuntimeException error) { - if (error instanceof CliRethrowException rethrow) { + if (error instanceof CliFatalException rethrow) { return rethrow; } else if (error instanceof CliException) { - return new CliRethrowException(error.getMessage(), error); + return new CliFatalException(error.getMessage(), error); } - return new CliRethrowException("Failed to set up the settings repository: " + error.getMessage(), error); + return new CliFatalException("Failed to set up the settings repository: " + error.getMessage(), error); } + //TODO: Reimplement this! private void pullSettings() { Path settingsPath = this.context.getSettingsPath(); @@ -215,12 +213,12 @@ private void moveSettings(RepositoryType repositoryType) { Path settingsPath = this.context.getSettingsPath(); if ((repositoryType == RepositoryType.SETTINGS) || (repositoryType == RepositoryType.UNKNOWN)) { - moveProject(this.tempDir, settingsPath); + moveProject(this.tempRepoDir, settingsPath); this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); } else { // for a code repository we clone into the workspace and symlink IDE_HOME/settings to its settings folder Path codePath = this.context.getWorkspacePath().resolve(this.gitProjectName); - moveProject(this.tempDir, codePath); + moveProject(this.tempRepoDir, codePath); Path settingsFolder = codePath.resolve(IdeContext.FOLDER_SETTINGS); if (Files.isDirectory(settingsFolder)) { this.context.getFileAccess().symlink(settingsFolder, settingsPath); @@ -229,16 +227,22 @@ private void moveSettings(RepositoryType repositoryType) { LOG.warn("The repository has been cloned to {} but it does not contain a settings folder so your project has no settings.", codePath); } } - this.tempDir = null; + this.tempRepoDir = null; } - private Path cloneToTempDir(GitUrl gitUrl) { + /** + * Clone a settings repository into a temporary directory. + * @param gitUrl {@link GitUrl} of the (code-)settings repository. + * @return {@link Path} of the temporary directory. + */ + private Path cloneRepoToTempDir(GitUrl gitUrl) { this.gitProjectName = gitUrl.getProjectName(); + // createTempDir guarantees a unique and empty directory so no leftovers of a previous attempt can interfere and we can clone directly - this.tempDir = this.context.getFileAccess().createTempDir(this.gitProjectName + "-"); - this.context.getGitContext().clone(gitUrl, this.tempDir); - return this.tempDir; + this.tempRepoDir = this.context.getFileAccess().createTempDir("project-"+this.gitProjectName); + this.context.getGitContext().clone(gitUrl, this.tempRepoDir); + return this.tempRepoDir; } private void backupBrokenSettings(Path settingsPath) { @@ -246,7 +250,7 @@ private void backupBrokenSettings(Path settingsPath) { if ((settingsPath == null) || !Files.exists(settingsPath)) { return; } - FileAccess fileAccess = this.context.getFileAccess(); + if (!fileAccess.isEmptyDir(settingsPath)) { this.context.askToContinue(""" Your settings repository seems to be broken ('.git' folder not present). @@ -260,7 +264,7 @@ Your settings repository seems to be broken ('.git' folder not present). /** * @return {@code true} if the user explicitly wants to continue with an invalid repository, {@code false} otherwise. */ - private boolean confirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl) { + private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl) { if (!this.context.isForceMode()) { return false; @@ -270,7 +274,7 @@ private boolean confirmInvalidRepository(RepositoryType repositoryType, GitUrl g return true; } - private static boolean isSettingsRepository(RepositoryType repositoryType) { + private static boolean isSettingsOrCodeSettingsRepository(RepositoryType repositoryType) { return (repositoryType == RepositoryType.SETTINGS) || (repositoryType == RepositoryType.CODE_SETTINGS_COMBINED); } @@ -281,15 +285,15 @@ private static boolean isSettingsRepository(RepositoryType repositoryType) { */ public void cleanup() { - if (this.tempDir == null) { + if (this.tempRepoDir == null) { return; } try { - this.context.getFileAccess().delete(this.tempDir); + this.context.getFileAccess().delete(this.tempRepoDir); } catch (RuntimeException e) { - LOG.warn("Failed to delete temporary directory {}", this.tempDir, e); + LOG.warn("Failed to delete temporary directory {}", this.tempRepoDir, e); } - this.tempDir = null; + this.tempRepoDir = null; } private GitUrl getOrAskSettingsUrl() { @@ -328,7 +332,7 @@ private void moveProject(Path from, Path to) { this.context.getFileAccess().move(from, to); } catch (RuntimeException e) { // FileAccess already reports source, target and the Windows file-lock hint so we only escalate to a fatal error here - throw new CliRethrowException(e.getMessage(), e); + throw new CliFatalException(e.getMessage(), e); } } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java index 0b430e4dcb..305f175dc0 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java @@ -686,7 +686,7 @@ public Path getSettingsGitRepository() { Path settingsPath = getSettingsPath(); // check whether the settings path has a .git folder only if its not a symbolic link or junction - if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsCodeRepository()) { + if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isCombinedSettingsCodeRepository()) { LOG.error("Settings repository exists but is not a git repository."); return null; } @@ -694,7 +694,7 @@ public Path getSettingsGitRepository() { } @Override - public boolean isSettingsCodeRepository() { + public boolean isCombinedSettingsCodeRepository() { Path settingsPath = getSettingsPath(); if (settingsPath != null) { @@ -1481,7 +1481,7 @@ settingsRepository, getSettingsCommitIdPath()))) { */ private String determineSettingsUpdateMessage(Commandlet cmd) { boolean update = cmd instanceof UpdateCommandlet; - if (isSettingsCodeRepository()) { + if (isCombinedSettingsCodeRepository()) { if (update && (isForceMode() || isForcePull())) { return null; } diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java index 013591e2a8..e22cf3cd6b 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java @@ -597,7 +597,7 @@ default Path getRepositoriesPath() { /** * @return {@code true} if the settings repository is a symlink or a junction to a code-repository. */ - boolean isSettingsCodeRepository(); + boolean isCombinedSettingsCodeRepository(); /** * @return the {@link Path} to the file containing the last tracked commit Id of the settings repository. diff --git a/cli/src/test/java/com/devonfw/tools/ide/step/StepTest.java b/cli/src/test/java/com/devonfw/tools/ide/step/StepTest.java index bbc4d9332f..c4ff3587b5 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/step/StepTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/step/StepTest.java @@ -2,7 +2,7 @@ import org.junit.jupiter.api.Test; -import com.devonfw.tools.ide.cli.CliRethrowException; +import com.devonfw.tools.ide.cli.CliFatalException; import com.devonfw.tools.ide.context.AbstractIdeContextTest; import com.devonfw.tools.ide.context.IdeTestContext; import com.devonfw.tools.ide.log.IdeLogEntry; @@ -154,8 +154,8 @@ void testRunRethrowsForcedError() { Step step = context.newStep("Test-Step"); // act & assert assertThatThrownBy(() -> step.run(() -> { - throw new CliRethrowException("fatal error"); - })).isInstanceOf(CliRethrowException.class).hasMessage("fatal error"); + throw new CliFatalException("fatal error"); + })).isInstanceOf(CliFatalException.class).hasMessage("fatal error"); assertThat(step.isFailure()).isTrue(); } @@ -182,8 +182,8 @@ void testCallRethrowsForcedError() { Step step = context.newStep("Test-Step"); // act & assert assertThatThrownBy(() -> step.call(() -> { - throw new CliRethrowException("fatal error"); - }, () -> "fallback")).isInstanceOf(CliRethrowException.class).hasMessage("fatal error"); + throw new CliFatalException("fatal error"); + }, () -> "fallback")).isInstanceOf(CliFatalException.class).hasMessage("fatal error"); assertThat(step.isFailure()).isTrue(); } From ec9d3c2d73e8dc31732da6b26a131962bf6b114a Mon Sep 17 00:00:00 2001 From: laim2003 Date: Thu, 27 Aug 2026 14:11:07 +0200 Subject: [PATCH 69/89] #1695: added warning for invalid Git urls provided via a parameter. --- .../tools/ide/commandlet/update/settings/SettingsUpdater.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index 6df8c24f9a..c7ca0d43a8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -3,6 +3,7 @@ import java.nio.file.Files; import java.nio.file.Path; +import org.jline.utils.Log; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -308,6 +309,7 @@ private GitUrl getOrAskSettingsUrl() { } String userPrompt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; while ((gitUrl == null) || !gitUrl.isValid()) { + LOG.warn("The provided git url parameter {} was detected to be invalid. Please enter a valid settings url.", gitUrl); repository = handleDefaultRepository(this.context.askForInput(userPrompt, IdeContext.DEFAULT_SETTINGS_REPO_URL)); gitUrl = GitUrl.of(repository); if (!gitUrl.isValid()) { From 129095d5c4f90e444b6b8e9335cadc435cf8ba4d Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 28 Aug 2026 13:34:36 +0200 Subject: [PATCH 70/89] #1695: small fixes --- .../update/AbstractUpdateCommandlet.java | 32 ++++++++++--------- .../update/settings/SettingsUpdateResult.java | 9 ++++-- .../ide/git/repository/RepositoryUtil.java | 2 +- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 5019e54430..570688b53a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -9,11 +9,13 @@ import java.util.Set; import java.util.stream.Stream; +import com.devonfw.tools.ide.cli.CliFatalException; import com.devonfw.tools.ide.commandlet.update.settings.HealthCheckResultStatus; import com.devonfw.tools.ide.commandlet.update.settings.SettingsHealthCheckResult; import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdateResult; import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdater; +import org.jline.utils.Log; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -186,20 +188,21 @@ private void updateSettingsInStep(Step step) { try { //Step 1: Perform health check Step healthCheckStep = this.context.newStep("Performing settings health check"); - Path temporaryRepoDir = healthCheckStep.call(() -> { - SettingsHealthCheckResult healthCheckResult = settingsUpdater.checkSettings(this.context.getSettingsPath()); - HealthCheckResultStatus status = healthCheckResult.status(); + SettingsHealthCheckResult healthCheckResult; + healthCheckResult = healthCheckStep.call(() -> { + SettingsHealthCheckResult _healthCheckResult = settingsUpdater.checkSettings(this.context.getSettingsPath()); + HealthCheckResultStatus status = _healthCheckResult.status(); if (status == null) { - healthCheckStep.error("Health check on settings failed due to unknown error - the settings have not been updated."); - return healthCheckResult.temporarySettingsDirectory(); - } else if (healthCheckResult.status() == HealthCheckResultStatus.SETTINGS_INVALID) { - healthCheckStep.error("The settings have not been updated: {}", healthCheckResult.errorMessage()); - return healthCheckResult.temporarySettingsDirectory(); + throw new CliFatalException("Health check on settings failed due to unknown error - the settings have not been updated"); + } else if (status == HealthCheckResultStatus.SETTINGS_INVALID) { + throw new CliFatalException("The settings could not be updated: " + _healthCheckResult.errorMessage()); } - return healthCheckResult.temporarySettingsDirectory(); + return _healthCheckResult; }, () -> null); - if(temporaryRepoDir == null || healthCheckStep.isFailure()) return; + if (healthCheckResult == null) { + throw new CliFatalException("Health check on settings failed due to unknown error - the settings have not been updated"); + } //Step 2: Let create/update commandlets prepare themselves for the settings update. onSettingHealthCheckSucceeded(); @@ -207,17 +210,16 @@ private void updateSettingsInStep(Step step) { //Step 3: Apply (move/pull newest version) settings Step applySettingsStep = this.context.newStep("Applying settings"); applySettingsStep.run(() -> { - SettingsUpdateResult settingsUpdateResult = settingsUpdater.applySettings(temporaryRepoDir); + SettingsUpdateResult settingsUpdateResult = settingsUpdater.applySettings(healthCheckResult.status() == HealthCheckResultStatus.SETTINGS_VALID_EXISTING, + healthCheckResult.temporarySettingsDirectory()); if (settingsUpdateResult == null) { - applySettingsStep.error("Failed to apply the settings update due to unknown error."); - return; + throw new CliFatalException("Failed to apply the settings update due to unknown error."); } switch (settingsUpdateResult.updateStatus()) { case SETTINGS_UPDATED -> applySettingsStep.success("Settings update successfully applied"); case SETTINGS_CLONED -> applySettingsStep.success("Settings successfully applied (cloned)"); - case SETTINGS_UPDATE_FAILED -> applySettingsStep.error("The settings update could not be applied: {}", settingsUpdateResult.errorMessage()); - case null, default -> applySettingsStep.error("Unexpected value: {}", settingsUpdateResult.updateStatus()); + case SETTINGS_UPDATE_FAILED -> throw new CliFatalException("The settings update could not be applied: " + settingsUpdateResult.errorMessage()); } }); } finally { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateResult.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateResult.java index 27b82b2d4c..87d3fe1e02 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateResult.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdateResult.java @@ -2,6 +2,9 @@ import com.devonfw.tools.ide.git.repository.RepositoryType; -public record SettingsUpdateResult(SettingsUpdateStatus updateStatus, RepositoryType repositoryType, String errorMessage) { - -} +/** + * @param updateStatus resulting status of the update operation + * @param repositoryType detected type of the repository + * @param errorMessage error message if updateStatus = {@link SettingsUpdateStatus}.SETTINGS_UPDATE_FAILED, otherwise {@code null} + */ +public record SettingsUpdateResult(SettingsUpdateStatus updateStatus, RepositoryType repositoryType, String errorMessage) {} diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 2eebe1d75d..722afc609b 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -20,7 +20,7 @@ public class RepositoryUtil { */ public static RepositoryType getRepositoryType(Path repositoryPath) { - if (!Files.isDirectory(repositoryPath)) { + if (repositoryPath == null || !Files.isDirectory(repositoryPath)) { return RepositoryType.UNKNOWN; } if (isSettingsFolder(repositoryPath)) { From 9e1da14c428813f430feda716c15e657bfbc1fd7 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 28 Aug 2026 15:24:38 +0200 Subject: [PATCH 71/89] #1695: - SettingsUpdater now respects force pull settings mode (--force-pull). - SettingsUpdater now correctly saves the commit Id after cloning/pulling. - Improved repositoryType with helper method --- .../update/AbstractUpdateCommandlet.java | 5 +- .../update/settings/SettingsUpdater.java | 111 +++++++++--------- .../ide/git/repository/RepositoryType.java | 9 +- 3 files changed, 65 insertions(+), 60 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 570688b53a..9a483ad272 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -184,7 +184,8 @@ protected String getStepMessage() { private void updateSettingsInStep(Step step) { - SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo); + //TODO: Only check for forcePull flag or also context.forceMode? + SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo, (this.forcePull.isTrue())); try { //Step 1: Perform health check Step healthCheckStep = this.context.newStep("Performing settings health check"); @@ -212,10 +213,10 @@ private void updateSettingsInStep(Step step) { applySettingsStep.run(() -> { SettingsUpdateResult settingsUpdateResult = settingsUpdater.applySettings(healthCheckResult.status() == HealthCheckResultStatus.SETTINGS_VALID_EXISTING, healthCheckResult.temporarySettingsDirectory()); - if (settingsUpdateResult == null) { throw new CliFatalException("Failed to apply the settings update due to unknown error."); } + switch (settingsUpdateResult.updateStatus()) { case SETTINGS_UPDATED -> applySettingsStep.success("Settings update successfully applied"); case SETTINGS_CLONED -> applySettingsStep.success("Settings successfully applied (cloned)"); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index c7ca0d43a8..6a66aa0891 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -3,7 +3,6 @@ import java.nio.file.Files; import java.nio.file.Path; -import org.jline.utils.Log; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,9 +20,9 @@ /** * Handles the settings repository of the current project in two phases: *
    - *
  1. {@link #checkSettings() health check}: the settings are always cloned into a temporary directory first where it is verified that the git URL is valid, + *
  2. {@link #checkSettings(Path)} health check: the settings are always cloned into a temporary directory first where it is verified that the git URL is valid, * that cloning succeeded, and that the repository actually is a settings or a combined code and settings repository.
  3. - *
  4. {@link #applySettings(SettingsHealthCheckResult) apply}: only after the health check succeeded the settings are either pulled in place (if they were already + *
  5. {@link #applySettings(boolean, Path)} apply: only after the health check succeeded the settings are either pulled in place (if they were already * present) or the verified clone is moved to its final location.
  6. *
*/ @@ -52,18 +51,21 @@ public class SettingsUpdater { /** The name of the git project - required to place a combined code and settings repository into the workspace. */ private String gitProjectName; + private boolean isForceMode; + /** * The constructor. * * @param context the {@link IdeContext}. * @param settingsRepoProperty the {@link StringProperty} with the settings repository URL from the update commandlet. */ - public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty) { + public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty, boolean isForceMode) { super(); this.context = context; this.settingsRepoProperty = settingsRepoProperty; this.fileAccess = context.getFileAccess(); + this.isForceMode = isForceMode; } /** @@ -79,7 +81,7 @@ public SettingsHealthCheckResult checkSettings(Path settingsPath) { // for a combined code and settings repository IDE_HOME/settings is a symlink into the code repository whose '.git' folder is one level above, // so isGitRepo would report it as broken settings RepositoryType settingsRepoType = RepositoryUtil.getRepositoryType(settingsPath); - if (isSettingsOrCodeSettingsRepository(settingsRepoType)) { + if (settingsRepoType.isSettingsOrCodeSettingsRepository()) { return checkSettingsPresent(settingsPath, settingsRepoType); } } @@ -87,49 +89,70 @@ public SettingsHealthCheckResult checkSettings(Path settingsPath) { } /** - * Applies the result of the {@link #checkSettings() health check} by either pulling the settings in place or moving the verified clone to its final + * Applies the result of the {@link #checkSettings(Path)} health check by either pulling the settings in place or moving the verified clone to its final * location. * + * @param onlyPull if true, we simply perform a git pull on the actual (not the one in the temp directory) settings repository. * @param sourcePath sourcePath of the settings to apply. * @return a {@link SettingsUpdateResult} representing the state, whether moving/pulling the newest settings was successful. */ - public SettingsUpdateResult applySettings(Path sourcePath) { + public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { RepositoryType repositoryType = RepositoryUtil.getRepositoryType(sourcePath); + Path settingsPath = this.context.getSettingsPath(); + + // Case 1: We performed "ide update"; so settings already existed and we just need to perform a git pull in the existing repo. + if (onlyPull) { + repositoryType = RepositoryUtil.getRepositoryType(context.getSettingsPath()); + if(repositoryType != RepositoryType.SETTINGS) { + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Expected settings repository for update."); + } + pullSettingsAndSaveCommitId(settingsPath); + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATED, repositoryType, null); + } + + // Case 2: We freshly cloned the settings repo and need to move it to a target directory. switch (repositoryType) { - case CODE -> { - //Technically should be caught during a health check, but we still handle this here. + case CODE, UNKNOWN -> { - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, MESSAGE_INVALID_REPOSITORY); + return moveSettingsOnlyIfForceMode(sourcePath, repositoryType); } case SETTINGS -> { //move to IDE_HOME/SETTINGS - moveProject(sourcePath, context.getSettingsPath()); + moveProject(sourcePath, settingsPath); + this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); } case CODE_SETTINGS_COMBINED -> { //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. (Formerly managed by the obsolete "--code" flag) - Path targetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); - moveProject(sourcePath, targetDirectory); - + Path repoMoveTargetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); - Path symlinkTargetPath = this.context.getWorkspacePath().resolve(gitProjectName).resolve(IdeContext.FOLDER_SETTINGS); + Path repoSettingsDirectory = repoMoveTargetDirectory.resolve(IdeContext.FOLDER_SETTINGS); - context.getFileAccess().symlink(symlinkTargetPath, symlinkPath); - this.context.getGitContext().saveCurrentCommitId(symlinkTargetPath, this.context.getSettingsCommitIdPath()); - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); - } - case UNKNOWN -> { + moveProject(sourcePath, repoMoveTargetDirectory); + + context.getFileAccess().symlink(repoSettingsDirectory, symlinkPath); - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, MESSAGE_INVALID_REPOSITORY); + this.context.getGitContext().saveCurrentCommitId(repoSettingsDirectory, this.context.getSettingsCommitIdPath()); + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); } } return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Unknown error during settings"); } + private SettingsUpdateResult moveSettingsOnlyIfForceMode(Path sourcePath, RepositoryType repositoryType) { + if(this.isForceMode) { + moveProject(sourcePath, this.context.getSettingsPath()); + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); + } else { + //Technically should be caught during a health check, but we still handle this here. + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, MESSAGE_INVALID_REPOSITORY); + } + } + /** * Health check for settings that are already present. As the project keeps working with these settings, a failure is only fatal if the user explicitly * aborted. Here, if a new version is available, we clone the new version into a temporary folder and perform health checks. If the cloned, new version is valid, @@ -144,12 +167,12 @@ private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, Reposi cleanup(); //If cloned repo is not (code-)settings repo and no force override (e.g. force mode) is applied, return error. - if (!isSettingsOrCodeSettingsRepository(clonedType) && !requestUserConfirmInvalidRepository(clonedType, gitUrl)) { + if (!clonedType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(clonedType, gitUrl)) { return SettingsHealthCheckResult.failed(clonedType, MESSAGE_INVALID_REPOSITORY, settingsPath); } //Otherwise, (e.g. user overrides), return valid. - return SettingsHealthCheckResult.of(HealthCheckResultStatus.SETTINGS_VALID, repositoryType, settingsPath); + return SettingsHealthCheckResult.of(HealthCheckResultStatus.SETTINGS_VALID_EXISTING, repositoryType, settingsPath); } catch (RuntimeException e) { cleanup(); if (e instanceof CliAbortException) { @@ -171,7 +194,7 @@ private SettingsHealthCheckResult checkClonedSettings(Path settingsPath) { Path tempCloneDir = cloneRepoToTempDir(gitUrl); RepositoryType repositoryType = RepositoryUtil.getRepositoryType(tempCloneDir); - if (!isSettingsOrCodeSettingsRepository(repositoryType) && !requestUserConfirmInvalidRepository(repositoryType, gitUrl)) { + if (!repositoryType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(repositoryType, gitUrl)) { //see @javadoc why we throw fatally here. throw new CliFatalException(MESSAGE_INVALID_REPOSITORY); } @@ -194,13 +217,11 @@ private static CliFatalException createGuaranteedFatalException(RuntimeException } else if (error instanceof CliException) { return new CliFatalException(error.getMessage(), error); } - return new CliFatalException("Failed to set up the settings repository: " + error.getMessage(), error); + return new CliFatalException("Error occurred during settings update: " + error.getClass() + ": " + error.getMessage(), error); } - //TODO: Reimplement this! - private void pullSettings() { + private void pullSettingsAndSaveCommitId(Path settingsPath) { - Path settingsPath = this.context.getSettingsPath(); GitContext gitContext = this.context.getGitContext(); if (gitContext.hasUntrackedFiles(settingsPath)) { gitContext.pullSafelyWithStash(settingsPath); @@ -210,27 +231,6 @@ private void pullSettings() { gitContext.saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); } - private void moveSettings(RepositoryType repositoryType) { - - Path settingsPath = this.context.getSettingsPath(); - if ((repositoryType == RepositoryType.SETTINGS) || (repositoryType == RepositoryType.UNKNOWN)) { - moveProject(this.tempRepoDir, settingsPath); - this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); - } else { - // for a code repository we clone into the workspace and symlink IDE_HOME/settings to its settings folder - Path codePath = this.context.getWorkspacePath().resolve(this.gitProjectName); - moveProject(this.tempRepoDir, codePath); - Path settingsFolder = codePath.resolve(IdeContext.FOLDER_SETTINGS); - if (Files.isDirectory(settingsFolder)) { - this.context.getFileAccess().symlink(settingsFolder, settingsPath); - this.context.getGitContext().saveCurrentCommitId(settingsFolder, this.context.getSettingsCommitIdPath()); - } else { - LOG.warn("The repository has been cloned to {} but it does not contain a settings folder so your project has no settings.", codePath); - } - } - this.tempRepoDir = null; - } - /** * Clone a settings repository into a temporary directory. * @param gitUrl {@link GitUrl} of the (code-)settings repository. @@ -266,18 +266,15 @@ Your settings repository seems to be broken ('.git' folder not present). * @return {@code true} if the user explicitly wants to continue with an invalid repository, {@code false} otherwise. */ private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl) { + LOG.warn("{}\nURL: {}\nDetected repository type: {}", MESSAGE_INVALID_REPOSITORY, gitUrl, repositoryType); - if (!this.context.isForceMode()) { + // If we are in force mode, we give the user the option continue with a potentially invalid repo. If not in FM, we skip asking and act as if he declined. + if(!this.isForceMode) { return false; } - LOG.warn("{}\nURL: {}\nDetected repository type: {}", MESSAGE_INVALID_REPOSITORY, gitUrl, repositoryType); - this.context.askToContinue("Force mode is active. Do you want to continue anyway?"); - return true; - } - - private static boolean isSettingsOrCodeSettingsRepository(RepositoryType repositoryType) { - return (repositoryType == RepositoryType.SETTINGS) || (repositoryType == RepositoryType.CODE_SETTINGS_COMBINED); + this.context.askToContinue("The (update of the) settings repository you are trying to apply seems to be broken. Do you want to continue anyway?"); + return true; } /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java index 81195bc178..2a4a4e5a5e 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java @@ -15,5 +15,12 @@ public enum RepositoryType { CODE_SETTINGS_COMBINED, /** The type of the repository could not be determined. */ - UNKNOWN + UNKNOWN; + + /** + * @return true if repository is either of type {@code SETTINGS} or {@code CODE_SETTINGS_COMBINED} + */ + public boolean isSettingsOrCodeSettingsRepository() { + return this == SETTINGS || this == CODE_SETTINGS_COMBINED; + } } From 8892e8829cc862947d598390c1508d37f5ac7a15 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 28 Aug 2026 17:17:38 +0200 Subject: [PATCH 72/89] #1695: - Small fixes of force mode handling in SettingsUpdater for the case of `ide update` - Changed error strategy in AbstractUpdateCommandlet to use CliException for non-fatal cases instead of step.error() with return - --- .../ide/commandlet/CreateCommandlet.java | 2 +- .../update/AbstractUpdateCommandlet.java | 17 ++++++++++------- .../update/settings/SettingsUpdater.java | 19 ++++++++++++------- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index e5c5b24b82..9bf6014420 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -68,7 +68,7 @@ protected void doRun() { } @Override - protected void onSettingHealthCheckSucceeded() { + protected void onSettingHealthCheckFinished() { // only called after the settings passed the health check Path newProjectPath = getNewProjectPath(); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 9a483ad272..85934fc8c8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -9,13 +9,13 @@ import java.util.Set; import java.util.stream.Stream; +import com.devonfw.tools.ide.cli.CliException; import com.devonfw.tools.ide.cli.CliFatalException; import com.devonfw.tools.ide.commandlet.update.settings.HealthCheckResultStatus; import com.devonfw.tools.ide.commandlet.update.settings.SettingsHealthCheckResult; import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdateResult; import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdater; -import org.jline.utils.Log; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -107,7 +107,7 @@ protected void doRun() { * Hook that is called after the settings passed the health check but before they are moved to their final location. Does nothing by default and is overridden * by {@link CreateCommandlet} to create the project structure so that no project is created at all if the health check failed. */ - protected void onSettingHealthCheckSucceeded() { + protected void onSettingHealthCheckFinished() { // nothing to do by default } @@ -195,22 +195,25 @@ private void updateSettingsInStep(Step step) { HealthCheckResultStatus status = _healthCheckResult.status(); if (status == null) { - throw new CliFatalException("Health check on settings failed due to unknown error - the settings have not been updated"); + throw new CliException("Health check on settings failed due to unknown error - the settings have not been updated"); } else if (status == HealthCheckResultStatus.SETTINGS_INVALID) { - throw new CliFatalException("The settings could not be updated: " + _healthCheckResult.errorMessage()); + throw new CliException("The settings health check failed: " + _healthCheckResult.errorMessage()); } return _healthCheckResult; }, () -> null); - if (healthCheckResult == null) { - throw new CliFatalException("Health check on settings failed due to unknown error - the settings have not been updated"); + + //If health check failed and force mode is disabled, skip application of settings. + if(!this.forcePull.isTrue() && (healthCheckResult == null || healthCheckStep.isFailure())) { + throw new CliException("Settings update aborted due to error in health check"); } //Step 2: Let create/update commandlets prepare themselves for the settings update. - onSettingHealthCheckSucceeded(); + onSettingHealthCheckFinished(); //Step 3: Apply (move/pull newest version) settings Step applySettingsStep = this.context.newStep("Applying settings"); applySettingsStep.run(() -> { + SettingsUpdateResult settingsUpdateResult = settingsUpdater.applySettings(healthCheckResult.status() == HealthCheckResultStatus.SETTINGS_VALID_EXISTING, healthCheckResult.temporarySettingsDirectory()); if (settingsUpdateResult == null) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index 6a66aa0891..b3ecebb36e 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -104,7 +104,7 @@ public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { // Case 1: We performed "ide update"; so settings already existed and we just need to perform a git pull in the existing repo. if (onlyPull) { repositoryType = RepositoryUtil.getRepositoryType(context.getSettingsPath()); - if(repositoryType != RepositoryType.SETTINGS) { + if(repositoryType != RepositoryType.SETTINGS && !this.isForceMode) { return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Expected settings repository for update."); } @@ -116,16 +116,17 @@ public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { switch (repositoryType) { case CODE, UNKNOWN -> { - return moveSettingsOnlyIfForceMode(sourcePath, repositoryType); + return moveSettingsOnlyIfForceModeActive(sourcePath, repositoryType); } case SETTINGS -> { - //move to IDE_HOME/SETTINGS + //move to IDE_HOME/SETTINGS moveProject(sourcePath, settingsPath); this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); } case CODE_SETTINGS_COMBINED -> { + //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. (Formerly managed by the obsolete "--code" flag) Path repoMoveTargetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); @@ -143,7 +144,8 @@ public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Unknown error during settings"); } - private SettingsUpdateResult moveSettingsOnlyIfForceMode(Path sourcePath, RepositoryType repositoryType) { + private SettingsUpdateResult moveSettingsOnlyIfForceModeActive(Path sourcePath, RepositoryType repositoryType) { + LOG.warn("Force mode is active: Moving potentially invalid settings repository to {}", this.context.getSettingsPath()); if(this.isForceMode) { moveProject(sourcePath, this.context.getSettingsPath()); return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); @@ -177,7 +179,7 @@ private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, Reposi cleanup(); if (e instanceof CliAbortException) { // the user answered "no" so we must not silently carry on - throw createGuaranteedFatalException(e); + return SettingsHealthCheckResult.failed(repositoryType, "Settings update aborted by end-user", settingsPath); } return SettingsHealthCheckResult.failed(repositoryType, e.getMessage(), settingsPath); } @@ -194,6 +196,7 @@ private SettingsHealthCheckResult checkClonedSettings(Path settingsPath) { Path tempCloneDir = cloneRepoToTempDir(gitUrl); RepositoryType repositoryType = RepositoryUtil.getRepositoryType(tempCloneDir); + if (!repositoryType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(repositoryType, gitUrl)) { //see @javadoc why we throw fatally here. throw new CliFatalException(MESSAGE_INVALID_REPOSITORY); @@ -266,9 +269,11 @@ Your settings repository seems to be broken ('.git' folder not present). * @return {@code true} if the user explicitly wants to continue with an invalid repository, {@code false} otherwise. */ private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl) { - LOG.warn("{}\nURL: {}\nDetected repository type: {}", MESSAGE_INVALID_REPOSITORY, gitUrl, repositoryType); + LOG.warn("{}\nURL: {}\nDetected settings repository type: {}", MESSAGE_INVALID_REPOSITORY, gitUrl, repositoryType); - // If we are in force mode, we give the user the option continue with a potentially invalid repo. If not in FM, we skip asking and act as if he declined. + /* If we are in force mode, we give the user the option continue with a potentially invalid repo. If not in FM, we skip asking and act as if he declined. + For the case of updating existing repositories, we always want to ask the user regardless of --force-pull + */ if(!this.isForceMode) { return false; } From 927328b0eaad87c356b99d03e18c5bb04c36690f Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 28 Aug 2026 17:34:38 +0200 Subject: [PATCH 73/89] #1695: added javadocs --- .../settings/HealthCheckResultStatus.java | 4 +++- .../settings/SettingsHealthCheckResult.java | 13 +++++++----- .../update/settings/SettingsUpdater.java | 20 ++++++++++--------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java index 673ef4f00b..97ae1588e9 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java @@ -1,7 +1,9 @@ package com.devonfw.tools.ide.commandlet.update.settings; +import java.nio.file.Path; + /** - * Status of the settings {@link SettingsUpdater#checkSettings() health check} describing what {@link SettingsUpdater#applySettings(SettingsHealthCheckResult)} has + * Status of the settings {@link SettingsUpdater#checkSettings(Path)} health check} describing what {@link SettingsUpdater#applySettings(boolean, Path)} has * to do. */ public enum HealthCheckResultStatus { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java index e28692c112..e644a03f56 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java @@ -5,31 +5,34 @@ import java.nio.file.Path; /** - * Result of the settings {@link SettingsUpdater#checkSettings() health check}. + * Result of the settings {@link SettingsUpdater#checkSettings(Path)} health check}. * * @param status the {@link HealthCheckResultStatus}. * @param repositoryType the {@link RepositoryType} of the settings repository. * @param errorMessage the reason why the settings could not be updated or {@code null} if the health check succeeded. + * @param temporarySettingsDirectory path to the temporary folder this health check was performed on. */ public record SettingsHealthCheckResult(HealthCheckResultStatus status, RepositoryType repositoryType, Path temporarySettingsDirectory, String errorMessage) { /** * @param status the {@link HealthCheckResultStatus}. * @param repositoryType the {@link RepositoryType}. + * @param temporarySettingsDirectory path to the temporary folder this health check was performed on. * @return a {@link SettingsHealthCheckResult} for a successful health check. */ - public static SettingsHealthCheckResult of(HealthCheckResultStatus status, RepositoryType repositoryType, Path temporaryRepoDirectory) { + public static SettingsHealthCheckResult of(HealthCheckResultStatus status, RepositoryType repositoryType, Path temporarySettingsDirectory) { - return new SettingsHealthCheckResult(status, repositoryType, temporaryRepoDirectory, null); + return new SettingsHealthCheckResult(status, repositoryType, temporarySettingsDirectory, null); } /** * @param repositoryType the {@link RepositoryType} of the settings that are already present. * @param errorMessage the reason why the settings could not be updated. + * @param temporarySettingsDirectory path to the temporary folder this health check was performed on. * @return a {@link SettingsHealthCheckResult} for a failed but recoverable health check. */ - public static SettingsHealthCheckResult failed(RepositoryType repositoryType, String errorMessage, Path temporaryRepoDirectory) { + public static SettingsHealthCheckResult failed(RepositoryType repositoryType, String errorMessage, Path temporarySettingsDirectory) { - return new SettingsHealthCheckResult(HealthCheckResultStatus.SETTINGS_INVALID, repositoryType, temporaryRepoDirectory, errorMessage); + return new SettingsHealthCheckResult(HealthCheckResultStatus.SETTINGS_INVALID, repositoryType, temporarySettingsDirectory, errorMessage); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index b3ecebb36e..abad1a29dc 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -51,13 +51,14 @@ public class SettingsUpdater { /** The name of the git project - required to place a combined code and settings repository into the workspace. */ private String gitProjectName; - private boolean isForceMode; + private final boolean isForceMode; /** * The constructor. * * @param context the {@link IdeContext}. * @param settingsRepoProperty the {@link StringProperty} with the settings repository URL from the update commandlet. + * @param isForceMode if in force mode, the settings health check will always return either {@link HealthCheckResultStatus#SETTINGS_VALID} or {@link HealthCheckResultStatus#SETTINGS_VALID_EXISTING} */ public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty, boolean isForceMode) { @@ -73,6 +74,7 @@ public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty, * is backed up. Whether the settings are pulled or cloned is decided solely by the state of {@link IdeContext#getSettingsPath() IDE_HOME/settings} so that * {@code ide create} and {@code ide update} share the very same logic. * + * @param settingsPath the path to the (code-)settings directory which the health check should be performed on. * @return the {@link SettingsHealthCheckResult}. */ public SettingsHealthCheckResult checkSettings(Path settingsPath) { @@ -169,7 +171,7 @@ private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, Reposi cleanup(); //If cloned repo is not (code-)settings repo and no force override (e.g. force mode) is applied, return error. - if (!clonedType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(clonedType, gitUrl)) { + if (!clonedType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(clonedType, gitUrl, true)) { return SettingsHealthCheckResult.failed(clonedType, MESSAGE_INVALID_REPOSITORY, settingsPath); } @@ -197,7 +199,7 @@ private SettingsHealthCheckResult checkClonedSettings(Path settingsPath) { Path tempCloneDir = cloneRepoToTempDir(gitUrl); RepositoryType repositoryType = RepositoryUtil.getRepositoryType(tempCloneDir); - if (!repositoryType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(repositoryType, gitUrl)) { + if (!repositoryType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(repositoryType, gitUrl, false)) { //see @javadoc why we throw fatally here. throw new CliFatalException(MESSAGE_INVALID_REPOSITORY); } @@ -268,17 +270,17 @@ Your settings repository seems to be broken ('.git' folder not present). /** * @return {@code true} if the user explicitly wants to continue with an invalid repository, {@code false} otherwise. */ - private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl) { - LOG.warn("{}\nURL: {}\nDetected settings repository type: {}", MESSAGE_INVALID_REPOSITORY, gitUrl, repositoryType); - + private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl, boolean updatesExistingRepository) { /* If we are in force mode, we give the user the option continue with a potentially invalid repo. If not in FM, we skip asking and act as if he declined. - For the case of updating existing repositories, we always want to ask the user regardless of --force-pull + For the case of updating existing settings repositories, we always want to ask the user regardless of --force-pull, as this could break the setup. */ - if(!this.isForceMode) { + if(!this.isForceMode && !updatesExistingRepository) { return false; } - this.context.askToContinue("The (update of the) settings repository you are trying to apply seems to be broken. Do you want to continue anyway?"); + LOG.warn("{}\nURL: {}\nDetected settings repository type: {}", MESSAGE_INVALID_REPOSITORY, gitUrl, repositoryType); + + this.context.askToContinue("The update to the settings repository you are trying to apply seems to be broken. Do you want to continue anyway?"); return true; } From 2b8c940f6ea62e55b9526e83e3e82ceb3b1b7f27 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 28 Aug 2026 17:39:23 +0200 Subject: [PATCH 74/89] #1695: resolved checkstyle violations --- .../update/settings/SettingsUpdater.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index abad1a29dc..92706d8a8f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -20,8 +20,9 @@ /** * Handles the settings repository of the current project in two phases: *
    - *
  1. {@link #checkSettings(Path)} health check: the settings are always cloned into a temporary directory first where it is verified that the git URL is valid, - * that cloning succeeded, and that the repository actually is a settings or a combined code and settings repository.
  2. + *
  3. {@link #checkSettings(Path)} health check: the settings are always cloned into a temporary directory + * first where it is verified that the git URL is valid, that cloning succeeded, + * and that the repository actually is a settings or a combined code and settings repository.
  4. *
  5. {@link #applySettings(boolean, Path)} apply: only after the health check succeeded the settings are either pulled in place (if they were already * present) or the verified clone is moved to its final location.
  6. *
@@ -58,7 +59,8 @@ public class SettingsUpdater { * * @param context the {@link IdeContext}. * @param settingsRepoProperty the {@link StringProperty} with the settings repository URL from the update commandlet. - * @param isForceMode if in force mode, the settings health check will always return either {@link HealthCheckResultStatus#SETTINGS_VALID} or {@link HealthCheckResultStatus#SETTINGS_VALID_EXISTING} + * @param isForceMode if in force mode, the settings health check will always return either {@link HealthCheckResultStatus#SETTINGS_VALID} or + * {@link HealthCheckResultStatus#SETTINGS_VALID_EXISTING} */ public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty, boolean isForceMode) { @@ -129,7 +131,8 @@ public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { } case CODE_SETTINGS_COMBINED -> { - //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. (Formerly managed by the obsolete "--code" flag) + //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. + // (Formerly managed by the obsolete "--code" flag) Path repoMoveTargetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); Path repoSettingsDirectory = repoMoveTargetDirectory.resolve(IdeContext.FOLDER_SETTINGS); @@ -159,8 +162,8 @@ private SettingsUpdateResult moveSettingsOnlyIfForceModeActive(Path sourcePath, /** * Health check for settings that are already present. As the project keeps working with these settings, a failure is only fatal if the user explicitly - * aborted. Here, if a new version is available, we clone the new version into a temporary folder and perform health checks. If the cloned, new version is valid, - * we call git update in the existing settings folder. + * aborted. Here, if a new version is available, we clone the new version into a temporary folder and perform health checks. + * If the cloned, new version is valid, we call git update in the existing settings folder. */ private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, RepositoryType repositoryType) { From 94e5664b6bd987c9d17659816af7f84461201f35 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 28 Aug 2026 18:57:33 +0200 Subject: [PATCH 75/89] #1695: - fixed SettingsUpdater not recognizing context.isForceMode() - Fixed "Update settings" step not failing if "Apply update" step fails - RepositoryUtil now also checks for the presence of a .git folder - Fixed some altered log messages in UpdateCommandletTest --- .../commandlet/update/AbstractUpdateCommandlet.java | 7 ++++++- .../commandlet/update/settings/SettingsUpdater.java | 1 - .../tools/ide/git/repository/RepositoryUtil.java | 10 +++++++--- .../tools/ide/commandlet/UpdateCommandletTest.java | 5 ++--- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 85934fc8c8..1ca28f7904 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -185,7 +185,7 @@ protected String getStepMessage() { private void updateSettingsInStep(Step step) { //TODO: Only check for forcePull flag or also context.forceMode? - SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo, (this.forcePull.isTrue())); + SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo, (this.forcePull.isTrue() || this.context.isForceMode())); try { //Step 1: Perform health check Step healthCheckStep = this.context.newStep("Performing settings health check"); @@ -226,6 +226,11 @@ private void updateSettingsInStep(Step step) { case SETTINGS_UPDATE_FAILED -> throw new CliFatalException("The settings update could not be applied: " + settingsUpdateResult.errorMessage()); } }); + + //Make sure to always fail the parent step if the "Apply settings" step fails. + if(applySettingsStep.isFailure()) { + throw new CliException("Settings update failed due to error while applying the settings update"); + } } finally { // the verified clone lives across both steps and the prepareProject hook so it is only here that its lifetime ends settingsUpdater.cleanup(); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index 92706d8a8f..966d1df9bd 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -316,7 +316,6 @@ private GitUrl getOrAskSettingsUrl() { } String userPrompt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:"; while ((gitUrl == null) || !gitUrl.isValid()) { - LOG.warn("The provided git url parameter {} was detected to be invalid. Please enter a valid settings url.", gitUrl); repository = handleDefaultRepository(this.context.askForInput(userPrompt, IdeContext.DEFAULT_SETTINGS_REPO_URL)); gitUrl = GitUrl.of(repository); if (!gitUrl.isValid()) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 722afc609b..044148477f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -5,6 +5,9 @@ import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.environment.EnvironmentVariables; +import com.devonfw.tools.ide.git.GitContext; +import com.devonfw.tools.ide.git.GitContextImpl; +import com.devonfw.tools.ide.git.GitUrl; /** * Utility class for IDEasy settings/code repositories. @@ -33,7 +36,7 @@ public static RepositoryType getRepositoryType(Path repositoryPath) { if (!Files.exists(settingsFolder)) { return RepositoryType.CODE; } - // there is a settings folder but it does not contain the required properties file + // there is no valid settings folder to be found. return RepositoryType.UNKNOWN; } @@ -43,7 +46,8 @@ public static RepositoryType getRepositoryType(Path repositoryPath) { */ private static boolean isSettingsFolder(Path folder) { - return Files.exists(folder.resolve(EnvironmentVariables.DEFAULT_PROPERTIES)) - || Files.exists(folder.resolve(EnvironmentVariables.LEGACY_PROPERTIES)); + return Files.exists(folder.resolve(GitContext.GIT_FOLDER)) && + (Files.exists(folder.resolve(EnvironmentVariables.DEFAULT_PROPERTIES)) + || Files.exists(folder.resolve(EnvironmentVariables.LEGACY_PROPERTIES))); } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java index 8715b74b4e..a1efb8035a 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/UpdateCommandletTest.java @@ -28,7 +28,7 @@ class UpdateCommandletTest extends AbstractIdeContextTest { private static final String PROJECT_UPDATE = "update"; - private static final String SUCCESS_UPDATE_SETTINGS = "Successfully ended step 'update (pull) settings repository'."; + private static final String SUCCESS_UPDATE_SETTINGS = "Successfully ended step 'Update settings repository'."; private static final String SUCCESS_INSTALL_OR_UPDATE_SOFTWARE = "Install or update software"; @Test @@ -179,7 +179,6 @@ void testRunUpdateWithBrokenSettingsFolder() { // assert assertThat(context).logAtSuccess().hasMessage(SUCCESS_UPDATE_SETTINGS); - assertThat(context).logAtInfo().hasMessageContaining("Creating backup by moving " + settingsPath); assertThat(context.getIdeHome().resolve(IdeContext.FOLDER_BACKUPS)).exists(); assertThat(settingsPath.resolve(GitContext.GIT_FOLDER)).exists(); assertThat(context).logAtSuccess().hasMessageContaining(SUCCESS_INSTALL_OR_UPDATE_SOFTWARE); @@ -208,7 +207,7 @@ public void pull(Path repository) { update.run(); // assert - assertThat(context).logAtError().hasMessage("Step 'Applying update' ended with failure."); + assertThat(context).logAtError().hasMessage("Step 'Applying settings' ended with failure."); assertThat(context).log().hasNoMessage(SUCCESS_UPDATE_SETTINGS); assertThat(context).logAtSuccess().hasMessageContaining(SUCCESS_INSTALL_OR_UPDATE_SOFTWARE); } From 938b9569885a1a5df13bb28621e7c32855cf3ffe Mon Sep 17 00:00:00 2001 From: laim2003 Date: Mon, 31 Aug 2026 16:21:34 +0200 Subject: [PATCH 76/89] #1695: - fixed faulty type determination check in RepositoryUtil --- .../update/AbstractUpdateCommandlet.java | 14 +++++++------- .../tools/ide/git/repository/RepositoryUtil.java | 5 ++--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 1ca28f7904..58bce4e6b0 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -174,7 +174,7 @@ protected void updateSettings() { return; } Step step = this.context.newStep(getStepMessage()); - step.run(() -> updateSettingsInStep(step)); + step.run(this::updateSettingsInStep); } protected String getStepMessage() { @@ -182,9 +182,8 @@ protected String getStepMessage() { return "Update settings repository"; } - private void updateSettingsInStep(Step step) { + private void updateSettingsInStep() { - //TODO: Only check for forcePull flag or also context.forceMode? SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo, (this.forcePull.isTrue() || this.context.isForceMode())); try { //Step 1: Perform health check @@ -202,8 +201,8 @@ private void updateSettingsInStep(Step step) { return _healthCheckResult; }, () -> null); - //If health check failed and force mode is disabled, skip application of settings. - if(!this.forcePull.isTrue() && (healthCheckResult == null || healthCheckStep.isFailure())) { + //If health check failed and force mode is disabled, skip application of settings and fail "Update settings" step. + if(!this.forcePull.isTrue() && (healthCheckResult == null || healthCheckResult.status() == null || healthCheckStep.isFailure())) { throw new CliException("Settings update aborted due to error in health check"); } @@ -217,13 +216,14 @@ private void updateSettingsInStep(Step step) { SettingsUpdateResult settingsUpdateResult = settingsUpdater.applySettings(healthCheckResult.status() == HealthCheckResultStatus.SETTINGS_VALID_EXISTING, healthCheckResult.temporarySettingsDirectory()); if (settingsUpdateResult == null) { - throw new CliFatalException("Failed to apply the settings update due to unknown error."); + + throw new CliException("Failed to apply the settings update due to unknown error."); } switch (settingsUpdateResult.updateStatus()) { case SETTINGS_UPDATED -> applySettingsStep.success("Settings update successfully applied"); case SETTINGS_CLONED -> applySettingsStep.success("Settings successfully applied (cloned)"); - case SETTINGS_UPDATE_FAILED -> throw new CliFatalException("The settings update could not be applied: " + settingsUpdateResult.errorMessage()); + case SETTINGS_UPDATE_FAILED -> throw new CliException("The settings update could not be applied: " + settingsUpdateResult.errorMessage()); } }); diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 044148477f..f0efb73842 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -26,7 +26,7 @@ public static RepositoryType getRepositoryType(Path repositoryPath) { if (repositoryPath == null || !Files.isDirectory(repositoryPath)) { return RepositoryType.UNKNOWN; } - if (isSettingsFolder(repositoryPath)) { + if (isSettingsFolder(repositoryPath) && Files.exists(repositoryPath.resolve(GitContext.GIT_FOLDER))) { return RepositoryType.SETTINGS; } Path settingsFolder = repositoryPath.resolve(IdeContext.FOLDER_SETTINGS); @@ -46,8 +46,7 @@ public static RepositoryType getRepositoryType(Path repositoryPath) { */ private static boolean isSettingsFolder(Path folder) { - return Files.exists(folder.resolve(GitContext.GIT_FOLDER)) && - (Files.exists(folder.resolve(EnvironmentVariables.DEFAULT_PROPERTIES)) + return (Files.exists(folder.resolve(EnvironmentVariables.DEFAULT_PROPERTIES)) || Files.exists(folder.resolve(EnvironmentVariables.LEGACY_PROPERTIES))); } } From 32b18196bebb066ddb47bee32c2570445e3b26d5 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Mon, 31 Aug 2026 16:45:16 +0200 Subject: [PATCH 77/89] #1695: - javadoc adjustment - fixed checkstyle violations - --- .../tools/ide/commandlet/update/AbstractUpdateCommandlet.java | 1 - .../tools/ide/commandlet/update/settings/SettingsUpdater.java | 3 ++- .../com/devonfw/tools/ide/git/repository/RepositoryUtil.java | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 58bce4e6b0..89091843a8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -10,7 +10,6 @@ import java.util.stream.Stream; import com.devonfw.tools.ide.cli.CliException; -import com.devonfw.tools.ide.cli.CliFatalException; import com.devonfw.tools.ide.commandlet.update.settings.HealthCheckResultStatus; import com.devonfw.tools.ide.commandlet.update.settings.SettingsHealthCheckResult; import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdateResult; diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index 966d1df9bd..0b7547db49 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -191,7 +191,8 @@ private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, Reposi } /** - * Health check for missing or broken settings. Without valid settings there is nothing to continue with, so every failure is fatal here. + * Health check for missing or broken settings (e.g. {@code ide create}). + * Without valid settings there is nothing to continue with, so every failure is fatal here. */ private SettingsHealthCheckResult checkClonedSettings(Path settingsPath) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index f0efb73842..cebdaab3e5 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -6,8 +6,6 @@ import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.environment.EnvironmentVariables; import com.devonfw.tools.ide.git.GitContext; -import com.devonfw.tools.ide.git.GitContextImpl; -import com.devonfw.tools.ide.git.GitUrl; /** * Utility class for IDEasy settings/code repositories. From 5aca16e698364830b94b660c4aef2381658de9e6 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Mon, 31 Aug 2026 18:13:10 +0200 Subject: [PATCH 78/89] #1695: - fixed issue with IDEW_HOME/conf folder being created by setIdeHome before we actually completed the health check. --- .../com/devonfw/tools/ide/commandlet/CreateCommandlet.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index 9bf6014420..d9a10c3d1f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -58,9 +58,7 @@ protected void doRun() { this.context.askToContinue("Directory {} already exists. Do you want to continue?", newProjectPath); fileAccess.backup(newProjectPath); } - // point IDE_HOME to the new project before the settings are checked - this only computes the paths and creates nothing on disk so that a failing - // health check leaves no project behind. As IDE_HOME/settings does not exist yet the settings will be cloned instead of pulled. - this.context.setIdeHome(newProjectPath); + super.doRun(); this.context.getFileAccess().writeFileContent(IdeVersion.getVersionString(), newProjectPath.resolve(IdeContext.FILE_SOFTWARE_VERSION)); IdeLogLevel.SUCCESS.log(LOG, "Successfully created new project '{}'.", this.newProject.getValue()); @@ -72,8 +70,10 @@ protected void onSettingHealthCheckFinished() { // only called after the settings passed the health check Path newProjectPath = getNewProjectPath(); + FileAccess fileAccess = this.context.getFileAccess(); fileAccess.mkdirs(newProjectPath); + this.context.setIdeHome(newProjectPath); fileAccess.mkdirs(newProjectPath.resolve(IdeContext.FOLDER_SOFTWARE)); fileAccess.mkdirs(newProjectPath.resolve(IdeContext.FOLDER_PLUGINS)); fileAccess.mkdirs(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)); From 2d214d2426e19972d19df84598f66a2ebe3137df Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 2 Sep 2026 15:45:33 +0200 Subject: [PATCH 79/89] #1695: - fixed test issues - Changed SettingsUpdater error message - updated mock settings to valid but not real url - applied spotless plugin Signed-off-by: laim2003 --- .../tools/ide/commandlet/CreateCommandlet.java | 6 +++--- .../commandlet/update/AbstractUpdateCommandlet.java | 11 +++++------ .../update/settings/SettingsHealthCheckResult.java | 4 ++-- .../commandlet/update/settings/SettingsUpdater.java | 2 +- .../com/devonfw/tools/ide/git/GitContextMock.java | 4 ++-- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index d9a10c3d1f..9bf6014420 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -58,7 +58,9 @@ protected void doRun() { this.context.askToContinue("Directory {} already exists. Do you want to continue?", newProjectPath); fileAccess.backup(newProjectPath); } - + // point IDE_HOME to the new project before the settings are checked - this only computes the paths and creates nothing on disk so that a failing + // health check leaves no project behind. As IDE_HOME/settings does not exist yet the settings will be cloned instead of pulled. + this.context.setIdeHome(newProjectPath); super.doRun(); this.context.getFileAccess().writeFileContent(IdeVersion.getVersionString(), newProjectPath.resolve(IdeContext.FILE_SOFTWARE_VERSION)); IdeLogLevel.SUCCESS.log(LOG, "Successfully created new project '{}'.", this.newProject.getValue()); @@ -70,10 +72,8 @@ protected void onSettingHealthCheckFinished() { // only called after the settings passed the health check Path newProjectPath = getNewProjectPath(); - FileAccess fileAccess = this.context.getFileAccess(); fileAccess.mkdirs(newProjectPath); - this.context.setIdeHome(newProjectPath); fileAccess.mkdirs(newProjectPath.resolve(IdeContext.FOLDER_SOFTWARE)); fileAccess.mkdirs(newProjectPath.resolve(IdeContext.FOLDER_PLUGINS)); fileAccess.mkdirs(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 89091843a8..7e3ae827bd 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -9,18 +9,17 @@ import java.util.Set; import java.util.stream.Stream; -import com.devonfw.tools.ide.cli.CliException; -import com.devonfw.tools.ide.commandlet.update.settings.HealthCheckResultStatus; -import com.devonfw.tools.ide.commandlet.update.settings.SettingsHealthCheckResult; -import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdateResult; -import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdater; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.devonfw.tools.ide.cli.CliException; import com.devonfw.tools.ide.commandlet.Commandlet; import com.devonfw.tools.ide.commandlet.CommandletManager; import com.devonfw.tools.ide.commandlet.CreateCommandlet; +import com.devonfw.tools.ide.commandlet.update.settings.HealthCheckResultStatus; +import com.devonfw.tools.ide.commandlet.update.settings.SettingsHealthCheckResult; +import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdateResult; +import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdater; import com.devonfw.tools.ide.context.AbstractIdeContext; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.context.IdeStartContextImpl; diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java index e644a03f56..7481d7b4c7 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java @@ -1,9 +1,9 @@ package com.devonfw.tools.ide.commandlet.update.settings; -import com.devonfw.tools.ide.git.repository.RepositoryType; - import java.nio.file.Path; +import com.devonfw.tools.ide.git.repository.RepositoryType; + /** * Result of the settings {@link SettingsUpdater#checkSettings(Path)} health check}. * diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index 0b7547db49..46b50ba091 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -109,7 +109,7 @@ public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { if (onlyPull) { repositoryType = RepositoryUtil.getRepositoryType(context.getSettingsPath()); if(repositoryType != RepositoryType.SETTINGS && !this.isForceMode) { - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Expected settings repository for update."); + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Expected settings repository for update application, but was of type: " + repositoryType); } pullSettingsAndSaveCommitId(settingsPath); diff --git a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java index 27723760ac..30410bcbaa 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java @@ -20,8 +20,8 @@ */ public class GitContextMock extends GitContextImpl { - /** Fallback URL for repositories without a mocked {@code .git/config} - has to be a {@link GitUrl#isValid() valid} git URL. */ - private static final String MOCKED_URL_VALUE = DEFAULT_SETTINGS_GIT_URL; + /** Fallback URL for repositories without a mocked {@code .git/config} - has to be a valid git URL that is not the default settings URL, so both the settings health check and tool settings substitution (e.g. Maven) run. */ + private static final String MOCKED_URL_VALUE = "https://github.com/devonfw/mocked-settings.git"; /** Filename used to persist mocked remotes inside the {@code .git} folder. */ private static final String REMOTES_FILE = "remotes.properties"; From 92c72f7ab161ab6eb6d08fae219d6df263ae2faf Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 2 Sep 2026 15:57:41 +0200 Subject: [PATCH 80/89] #1695: fixed checkstyle violations Signed-off-by: laim2003 --- .../update/settings/SettingsUpdater.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index 46b50ba091..c7bfd55c23 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -60,7 +60,7 @@ public class SettingsUpdater { * @param context the {@link IdeContext}. * @param settingsRepoProperty the {@link StringProperty} with the settings repository URL from the update commandlet. * @param isForceMode if in force mode, the settings health check will always return either {@link HealthCheckResultStatus#SETTINGS_VALID} or - * {@link HealthCheckResultStatus#SETTINGS_VALID_EXISTING} + * {@link HealthCheckResultStatus#SETTINGS_VALID_EXISTING} */ public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty, boolean isForceMode) { @@ -108,8 +108,10 @@ public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { // Case 1: We performed "ide update"; so settings already existed and we just need to perform a git pull in the existing repo. if (onlyPull) { repositoryType = RepositoryUtil.getRepositoryType(context.getSettingsPath()); - if(repositoryType != RepositoryType.SETTINGS && !this.isForceMode) { - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Expected settings repository for update application, but was of type: " + repositoryType); + if (repositoryType != RepositoryType.SETTINGS && !this.isForceMode) { + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, + repositoryType, + "Expected settings repository for update application, but was of type: " + repositoryType); } pullSettingsAndSaveCommitId(settingsPath); @@ -151,7 +153,7 @@ public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { private SettingsUpdateResult moveSettingsOnlyIfForceModeActive(Path sourcePath, RepositoryType repositoryType) { LOG.warn("Force mode is active: Moving potentially invalid settings repository to {}", this.context.getSettingsPath()); - if(this.isForceMode) { + if (this.isForceMode) { moveProject(sourcePath, this.context.getSettingsPath()); return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); } else { @@ -162,8 +164,8 @@ private SettingsUpdateResult moveSettingsOnlyIfForceModeActive(Path sourcePath, /** * Health check for settings that are already present. As the project keeps working with these settings, a failure is only fatal if the user explicitly - * aborted. Here, if a new version is available, we clone the new version into a temporary folder and perform health checks. - * If the cloned, new version is valid, we call git update in the existing settings folder. + * aborted. Here, if a new version is available, we clone the new version into a temporary folder and perform health checks. If the cloned, new version is + * valid, we call git update in the existing settings folder. */ private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, RepositoryType repositoryType) { @@ -191,8 +193,8 @@ private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, Reposi } /** - * Health check for missing or broken settings (e.g. {@code ide create}). - * Without valid settings there is nothing to continue with, so every failure is fatal here. + * Health check for missing or broken settings (e.g. {@code ide create}). Without valid settings there is nothing to continue with, so every failure is fatal + * here. */ private SettingsHealthCheckResult checkClonedSettings(Path settingsPath) { @@ -242,6 +244,7 @@ private void pullSettingsAndSaveCommitId(Path settingsPath) { /** * Clone a settings repository into a temporary directory. + * * @param gitUrl {@link GitUrl} of the (code-)settings repository. * @return {@link Path} of the temporary directory. */ @@ -250,7 +253,7 @@ private Path cloneRepoToTempDir(GitUrl gitUrl) { this.gitProjectName = gitUrl.getProjectName(); // createTempDir guarantees a unique and empty directory so no leftovers of a previous attempt can interfere and we can clone directly - this.tempRepoDir = this.context.getFileAccess().createTempDir("project-"+this.gitProjectName); + this.tempRepoDir = this.context.getFileAccess().createTempDir("project-" + this.gitProjectName); this.context.getGitContext().clone(gitUrl, this.tempRepoDir); return this.tempRepoDir; } @@ -278,7 +281,7 @@ private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryTyp /* If we are in force mode, we give the user the option continue with a potentially invalid repo. If not in FM, we skip asking and act as if he declined. For the case of updating existing settings repositories, we always want to ask the user regardless of --force-pull, as this could break the setup. */ - if(!this.isForceMode && !updatesExistingRepository) { + if (!this.isForceMode && !updatesExistingRepository) { return false; } From ea693570e3a71f6a1d1125aed9d479ae3d00a6c3 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 2 Sep 2026 16:05:45 +0200 Subject: [PATCH 81/89] #1695: fixed checkstyle violations Signed-off-by: laim2003 --- .../com/devonfw/tools/ide/commandlet/StatusCommandlet.java | 3 ++- .../java/com/devonfw/tools/ide/git/GitContextMock.java | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java index 1cd9fe2fcd..dab3c83d5d 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java @@ -91,7 +91,8 @@ private void logSettingsLegacyStatus() { } if (hasLegacyProperties) { LOG.warn( - "Your settings are outdated and contain legacy configurations. Please consider upgrading your settings:\nhttps://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc#upgrade"); + "Your settings are outdated and contain legacy configurations. " + + "Please consider upgrading your settings:\nhttps://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc#upgrade"); } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java index 30410bcbaa..c2069af429 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java @@ -20,7 +20,10 @@ */ public class GitContextMock extends GitContextImpl { - /** Fallback URL for repositories without a mocked {@code .git/config} - has to be a valid git URL that is not the default settings URL, so both the settings health check and tool settings substitution (e.g. Maven) run. */ + /** + * Fallback URL for repositories without a mocked {@code .git/config} - has to be a valid git URL that is not the default settings URL, so both the settings + * health check and tool settings substitution (e.g. Maven) run succesfully (they check URL validity). + */ private static final String MOCKED_URL_VALUE = "https://github.com/devonfw/mocked-settings.git"; /** Filename used to persist mocked remotes inside the {@code .git} folder. */ @@ -264,7 +267,7 @@ public String determineRemote(Path repository) { return DEFAULT_REMOTE; } -/** + /** * Adds pending commits to simulate remote changes. Commits are stored per repository and applied on pull. * * @param repository the repository the commits belong to From 74a0dd9f48ff6286f1e7d1b62d57e7420dab4435 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Thu, 3 Sep 2026 13:22:07 +0200 Subject: [PATCH 82/89] #1695: removed force flag handling from SettingsUpdater as i misunderstood its purpose Signed-off-by: laim2003 --- .../update/AbstractUpdateCommandlet.java | 10 +++--- .../update/settings/SettingsUpdater.java | 31 +++---------------- .../ide/git/repository/RepositoryType.java | 2 +- .../ide/git/repository/RepositoryUtil.java | 2 +- 4 files changed, 13 insertions(+), 32 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 7e3ae827bd..3fac55198a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -182,7 +182,7 @@ protected String getStepMessage() { private void updateSettingsInStep() { - SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo, (this.forcePull.isTrue() || this.context.isForceMode())); + SettingsUpdater settingsUpdater = new SettingsUpdater(this.context, this.settingsRepo); try { //Step 1: Perform health check Step healthCheckStep = this.context.newStep("Performing settings health check"); @@ -199,8 +199,10 @@ private void updateSettingsInStep() { return _healthCheckResult; }, () -> null); - //If health check failed and force mode is disabled, skip application of settings and fail "Update settings" step. - if(!this.forcePull.isTrue() && (healthCheckResult == null || healthCheckResult.status() == null || healthCheckStep.isFailure())) { + // If the health check failed (healthCheckResult is null) the settings have not been verified, so skip applying them and fail the "Update settings" + // step. A non-null result is only produced when the health check passed or the user explicitly chose to continue anyway (force mode), so this never + // aborts in force mode. + if (healthCheckResult == null) { throw new CliException("Settings update aborted due to error in health check"); } @@ -226,7 +228,7 @@ private void updateSettingsInStep() { }); //Make sure to always fail the parent step if the "Apply settings" step fails. - if(applySettingsStep.isFailure()) { + if (applySettingsStep.isFailure()) { throw new CliException("Settings update failed due to error while applying the settings update"); } } finally { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index c7bfd55c23..4a825f2866 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -52,23 +52,18 @@ public class SettingsUpdater { /** The name of the git project - required to place a combined code and settings repository into the workspace. */ private String gitProjectName; - private final boolean isForceMode; - /** * The constructor. * * @param context the {@link IdeContext}. * @param settingsRepoProperty the {@link StringProperty} with the settings repository URL from the update commandlet. - * @param isForceMode if in force mode, the settings health check will always return either {@link HealthCheckResultStatus#SETTINGS_VALID} or - * {@link HealthCheckResultStatus#SETTINGS_VALID_EXISTING} */ - public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty, boolean isForceMode) { + public SettingsUpdater(IdeContext context, StringProperty settingsRepoProperty) { super(); this.context = context; this.settingsRepoProperty = settingsRepoProperty; this.fileAccess = context.getFileAccess(); - this.isForceMode = isForceMode; } /** @@ -108,7 +103,7 @@ public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { // Case 1: We performed "ide update"; so settings already existed and we just need to perform a git pull in the existing repo. if (onlyPull) { repositoryType = RepositoryUtil.getRepositoryType(context.getSettingsPath()); - if (repositoryType != RepositoryType.SETTINGS && !this.isForceMode) { + if (repositoryType != RepositoryType.SETTINGS) { return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Expected settings repository for update application, but was of type: " + repositoryType); @@ -120,9 +115,10 @@ public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { // Case 2: We freshly cloned the settings repo and need to move it to a target directory. switch (repositoryType) { - case CODE, UNKNOWN -> { + case PLAIN_CODE, UNKNOWN -> { - return moveSettingsOnlyIfForceModeActive(sourcePath, repositoryType); + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, + "Cannot apply settings as type of the settings repo is incorrect"); } case SETTINGS -> { @@ -151,17 +147,6 @@ public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Unknown error during settings"); } - private SettingsUpdateResult moveSettingsOnlyIfForceModeActive(Path sourcePath, RepositoryType repositoryType) { - LOG.warn("Force mode is active: Moving potentially invalid settings repository to {}", this.context.getSettingsPath()); - if (this.isForceMode) { - moveProject(sourcePath, this.context.getSettingsPath()); - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); - } else { - //Technically should be caught during a health check, but we still handle this here. - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, MESSAGE_INVALID_REPOSITORY); - } - } - /** * Health check for settings that are already present. As the project keeps working with these settings, a failure is only fatal if the user explicitly * aborted. Here, if a new version is available, we clone the new version into a temporary folder and perform health checks. If the cloned, new version is @@ -278,12 +263,6 @@ Your settings repository seems to be broken ('.git' folder not present). * @return {@code true} if the user explicitly wants to continue with an invalid repository, {@code false} otherwise. */ private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl, boolean updatesExistingRepository) { - /* If we are in force mode, we give the user the option continue with a potentially invalid repo. If not in FM, we skip asking and act as if he declined. - For the case of updating existing settings repositories, we always want to ask the user regardless of --force-pull, as this could break the setup. - */ - if (!this.isForceMode && !updatesExistingRepository) { - return false; - } LOG.warn("{}\nURL: {}\nDetected settings repository type: {}", MESSAGE_INVALID_REPOSITORY, gitUrl, repositoryType); diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java index 2a4a4e5a5e..b6d27fbac6 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java @@ -6,7 +6,7 @@ public enum RepositoryType { /** Git Repository is a code repository. */ - CODE, + PLAIN_CODE, /** Git Repository is a settings repository. */ SETTINGS, diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index cebdaab3e5..6e4001c3de 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -32,7 +32,7 @@ public static RepositoryType getRepositoryType(Path repositoryPath) { return RepositoryType.CODE_SETTINGS_COMBINED; } if (!Files.exists(settingsFolder)) { - return RepositoryType.CODE; + return RepositoryType.PLAIN_CODE; } // there is no valid settings folder to be found. return RepositoryType.UNKNOWN; From 330e4e1e64f9bb6de1038273f0c2c8d68d9e73aa Mon Sep 17 00:00:00 2001 From: laim2003 Date: Thu, 3 Sep 2026 14:39:18 +0200 Subject: [PATCH 83/89] #1695: - javadoc improvement - RepositoryUtil now also checks for git validity - formatting improvements in SettingsUpdater Signed-off-by: laim2003 --- .../update/AbstractUpdateCommandlet.java | 3 +- .../settings/HealthCheckResultStatus.java | 6 +- .../settings/SettingsHealthCheckResult.java | 13 +- .../update/settings/SettingsUpdater.java | 145 +++++++++--------- .../ide/git/repository/RepositoryUtil.java | 4 +- 5 files changed, 87 insertions(+), 84 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 3fac55198a..c67559d0d7 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -213,7 +213,8 @@ private void updateSettingsInStep() { Step applySettingsStep = this.context.newStep("Applying settings"); applySettingsStep.run(() -> { - SettingsUpdateResult settingsUpdateResult = settingsUpdater.applySettings(healthCheckResult.status() == HealthCheckResultStatus.SETTINGS_VALID_EXISTING, + boolean onlyPull = healthCheckResult.status() == HealthCheckResultStatus.SETTINGS_VALID && healthCheckResult.isExistingProject(); + SettingsUpdateResult settingsUpdateResult = settingsUpdater.applySettings(onlyPull, healthCheckResult.temporarySettingsDirectory()); if (settingsUpdateResult == null) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java index 97ae1588e9..5b6c9f45be 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java @@ -3,14 +3,12 @@ import java.nio.file.Path; /** - * Status of the settings {@link SettingsUpdater#checkSettings(Path)} health check} describing what {@link SettingsUpdater#applySettings(boolean, Path)} has - * to do. + * Status of the settings {@link SettingsUpdater#checkSettings(Path)} health check} describing what {@link SettingsUpdater#applySettings(boolean, Path)} has to + * do. */ public enum HealthCheckResultStatus { /** The settings repository was cloned to a temporary directory and is valid - it can be moved to its final location. */ SETTINGS_VALID, - /** The settings repository already existed and was cloned to a temporary directory and is valid - it can be moved to its final location. */ - SETTINGS_VALID_EXISTING, /** The settings repository is invalid */ SETTINGS_INVALID } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java index 7481d7b4c7..3fe59a3023 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java @@ -12,7 +12,8 @@ * @param errorMessage the reason why the settings could not be updated or {@code null} if the health check succeeded. * @param temporarySettingsDirectory path to the temporary folder this health check was performed on. */ -public record SettingsHealthCheckResult(HealthCheckResultStatus status, RepositoryType repositoryType, Path temporarySettingsDirectory, String errorMessage) { +public record SettingsHealthCheckResult(HealthCheckResultStatus status, RepositoryType repositoryType, Path temporarySettingsDirectory, String errorMessage, + boolean isExistingProject) { /** * @param status the {@link HealthCheckResultStatus}. @@ -20,9 +21,10 @@ public record SettingsHealthCheckResult(HealthCheckResultStatus status, Reposito * @param temporarySettingsDirectory path to the temporary folder this health check was performed on. * @return a {@link SettingsHealthCheckResult} for a successful health check. */ - public static SettingsHealthCheckResult of(HealthCheckResultStatus status, RepositoryType repositoryType, Path temporarySettingsDirectory) { + public static SettingsHealthCheckResult of(HealthCheckResultStatus status, RepositoryType repositoryType, Path temporarySettingsDirectory, + boolean isExistingProject) { - return new SettingsHealthCheckResult(status, repositoryType, temporarySettingsDirectory, null); + return new SettingsHealthCheckResult(status, repositoryType, temporarySettingsDirectory, null, isExistingProject); } /** @@ -31,8 +33,9 @@ public static SettingsHealthCheckResult of(HealthCheckResultStatus status, Repos * @param temporarySettingsDirectory path to the temporary folder this health check was performed on. * @return a {@link SettingsHealthCheckResult} for a failed but recoverable health check. */ - public static SettingsHealthCheckResult failed(RepositoryType repositoryType, String errorMessage, Path temporarySettingsDirectory) { + public static SettingsHealthCheckResult failed(RepositoryType repositoryType, String errorMessage, Path temporarySettingsDirectory, + boolean isExistingProject) { - return new SettingsHealthCheckResult(HealthCheckResultStatus.SETTINGS_INVALID, repositoryType, temporarySettingsDirectory, errorMessage); + return new SettingsHealthCheckResult(HealthCheckResultStatus.SETTINGS_INVALID, repositoryType, temporarySettingsDirectory, errorMessage, isExistingProject); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index 4a825f2866..50d21c9296 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -79,7 +79,7 @@ public SettingsHealthCheckResult checkSettings(Path settingsPath) { if (settingsPath != null && !fileAccess.isEmptyDir(settingsPath)) { // for a combined code and settings repository IDE_HOME/settings is a symlink into the code repository whose '.git' folder is one level above, // so isGitRepo would report it as broken settings - RepositoryType settingsRepoType = RepositoryUtil.getRepositoryType(settingsPath); + RepositoryType settingsRepoType = RepositoryUtil.getRepositoryType(settingsPath, this.context.getGitContext()); if (settingsRepoType.isSettingsOrCodeSettingsRepository()) { return checkSettingsPresent(settingsPath, settingsRepoType); } @@ -87,66 +87,6 @@ public SettingsHealthCheckResult checkSettings(Path settingsPath) { return checkClonedSettings(settingsPath); } - /** - * Applies the result of the {@link #checkSettings(Path)} health check by either pulling the settings in place or moving the verified clone to its final - * location. - * - * @param onlyPull if true, we simply perform a git pull on the actual (not the one in the temp directory) settings repository. - * @param sourcePath sourcePath of the settings to apply. - * @return a {@link SettingsUpdateResult} representing the state, whether moving/pulling the newest settings was successful. - */ - public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { - - RepositoryType repositoryType = RepositoryUtil.getRepositoryType(sourcePath); - Path settingsPath = this.context.getSettingsPath(); - - // Case 1: We performed "ide update"; so settings already existed and we just need to perform a git pull in the existing repo. - if (onlyPull) { - repositoryType = RepositoryUtil.getRepositoryType(context.getSettingsPath()); - if (repositoryType != RepositoryType.SETTINGS) { - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, - repositoryType, - "Expected settings repository for update application, but was of type: " + repositoryType); - } - - pullSettingsAndSaveCommitId(settingsPath); - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATED, repositoryType, null); - } - - // Case 2: We freshly cloned the settings repo and need to move it to a target directory. - switch (repositoryType) { - case PLAIN_CODE, UNKNOWN -> { - - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, - "Cannot apply settings as type of the settings repo is incorrect"); - } - case SETTINGS -> { - - //move to IDE_HOME/SETTINGS - moveProject(sourcePath, settingsPath); - this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); - } - case CODE_SETTINGS_COMBINED -> { - - //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. - // (Formerly managed by the obsolete "--code" flag) - Path repoMoveTargetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); - Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); - Path repoSettingsDirectory = repoMoveTargetDirectory.resolve(IdeContext.FOLDER_SETTINGS); - - moveProject(sourcePath, repoMoveTargetDirectory); - - context.getFileAccess().symlink(repoSettingsDirectory, symlinkPath); - - this.context.getGitContext().saveCurrentCommitId(repoSettingsDirectory, this.context.getSettingsCommitIdPath()); - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); - } - } - - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Unknown error during settings"); - } - /** * Health check for settings that are already present. As the project keeps working with these settings, a failure is only fatal if the user explicitly * aborted. Here, if a new version is available, we clone the new version into a temporary folder and perform health checks. If the cloned, new version is @@ -157,23 +97,23 @@ private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, Reposi try { //Get Git url of existing settings, clone newest version of them to temp dir GitUrl gitUrl = GitUrl.of(this.context.getGitContext().retrieveGitUrl(settingsPath)); - RepositoryType clonedType = RepositoryUtil.getRepositoryType(cloneRepoToTempDir(gitUrl)); + RepositoryType clonedType = RepositoryUtil.getRepositoryType(cloneRepoToTempDir(gitUrl), this.context.getGitContext()); cleanup(); //If cloned repo is not (code-)settings repo and no force override (e.g. force mode) is applied, return error. - if (!clonedType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(clonedType, gitUrl, true)) { - return SettingsHealthCheckResult.failed(clonedType, MESSAGE_INVALID_REPOSITORY, settingsPath); + if (!clonedType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(clonedType, gitUrl)) { + return SettingsHealthCheckResult.failed(clonedType, MESSAGE_INVALID_REPOSITORY, settingsPath, true); } //Otherwise, (e.g. user overrides), return valid. - return SettingsHealthCheckResult.of(HealthCheckResultStatus.SETTINGS_VALID_EXISTING, repositoryType, settingsPath); + return SettingsHealthCheckResult.of(HealthCheckResultStatus.SETTINGS_VALID, repositoryType, settingsPath, true); } catch (RuntimeException e) { cleanup(); if (e instanceof CliAbortException) { // the user answered "no" so we must not silently carry on - return SettingsHealthCheckResult.failed(repositoryType, "Settings update aborted by end-user", settingsPath); + return SettingsHealthCheckResult.failed(repositoryType, "Settings update aborted by end-user", settingsPath, true); } - return SettingsHealthCheckResult.failed(repositoryType, e.getMessage(), settingsPath); + return SettingsHealthCheckResult.failed(repositoryType, e.getMessage(), settingsPath, true); } } @@ -188,13 +128,13 @@ private SettingsHealthCheckResult checkClonedSettings(Path settingsPath) { GitUrl gitUrl = getOrAskSettingsUrl(); Path tempCloneDir = cloneRepoToTempDir(gitUrl); - RepositoryType repositoryType = RepositoryUtil.getRepositoryType(tempCloneDir); + RepositoryType repositoryType = RepositoryUtil.getRepositoryType(tempCloneDir, this.context.getGitContext()); - if (!repositoryType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(repositoryType, gitUrl, false)) { + if (!repositoryType.isSettingsOrCodeSettingsRepository()) { //see @javadoc why we throw fatally here. - throw new CliFatalException(MESSAGE_INVALID_REPOSITORY); + return SettingsHealthCheckResult.failed(repositoryType, MESSAGE_INVALID_REPOSITORY, tempCloneDir, false); } - return SettingsHealthCheckResult.of(HealthCheckResultStatus.SETTINGS_VALID, repositoryType, tempCloneDir); + return SettingsHealthCheckResult.of(HealthCheckResultStatus.SETTINGS_VALID, repositoryType, tempCloneDir, false); } catch (RuntimeException e) { cleanup(); throw createGuaranteedFatalException(e); @@ -262,7 +202,7 @@ Your settings repository seems to be broken ('.git' folder not present). /** * @return {@code true} if the user explicitly wants to continue with an invalid repository, {@code false} otherwise. */ - private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl, boolean updatesExistingRepository) { + private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl) { LOG.warn("{}\nURL: {}\nDetected settings repository type: {}", MESSAGE_INVALID_REPOSITORY, gitUrl, repositoryType); @@ -270,6 +210,67 @@ private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryTyp return true; } + /** + * Applies the result of the {@link #checkSettings(Path)} health check by either pulling the settings in place or moving the verified clone to its final + * location. + * + * @param onlyPull if true, we simply perform a git pull on the actual (not the one in the temp directory) settings repository. + * @param sourcePath sourcePath of the settings to apply. + * @return a {@link SettingsUpdateResult} representing the state, whether moving/pulling the newest settings was successful. + */ + public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { + + GitContext gitContext = this.context.getGitContext(); + RepositoryType repositoryType = RepositoryUtil.getRepositoryType(sourcePath, gitContext); + Path settingsPath = this.context.getSettingsPath(); + + // Case 1: We performed "ide update"; so settings already existed and we just need to perform a git pull in the existing repo. + if (onlyPull) { + repositoryType = RepositoryUtil.getRepositoryType(context.getSettingsPath(), gitContext); + if (repositoryType != RepositoryType.SETTINGS) { + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, + repositoryType, + "Expected settings repository for update application, but was of type: " + repositoryType); + } + + pullSettingsAndSaveCommitId(settingsPath); + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATED, repositoryType, null); + } + + // Case 2: We freshly cloned the settings repo and need to move it to a target directory. + switch (repositoryType) { + case PLAIN_CODE, UNKNOWN -> { + + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, + "Cannot apply settings as type of the settings repo is incorrect"); + } + case SETTINGS -> { + + //move to IDE_HOME/SETTINGS + moveProject(sourcePath, settingsPath); + this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); + } + case CODE_SETTINGS_COMBINED -> { + + //this is a special case - here we need to symlink from IDE_HOME/settings to IDE_HOME/workspaces/main/repo_name/settings. + // (Formerly managed by the obsolete "--code" flag) + Path repoMoveTargetDirectory = this.context.getWorkspacePath().resolve(gitProjectName); + Path symlinkPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_SETTINGS); + Path repoSettingsDirectory = repoMoveTargetDirectory.resolve(IdeContext.FOLDER_SETTINGS); + + moveProject(sourcePath, repoMoveTargetDirectory); + + context.getFileAccess().symlink(repoSettingsDirectory, symlinkPath); + + this.context.getGitContext().saveCurrentCommitId(repoSettingsDirectory, this.context.getSettingsCommitIdPath()); + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); + } + } + + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Unknown error during settings"); + } + /** * Removes the temporary clone. It is deleted and not backed up since it only contains a fresh clone without any user data and a backup would be created * inside {@link IdeContext#getIdeHome() IDE_HOME} that may not even exist yet. Failures are only logged so that the actual error never gets masked. diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 6e4001c3de..0bafb2a19a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -19,12 +19,12 @@ public class RepositoryUtil { * @param repositoryPath the {@link Path} to the repository to check. * @return the {@link RepositoryType} of the repository. */ - public static RepositoryType getRepositoryType(Path repositoryPath) { + public static RepositoryType getRepositoryType(Path repositoryPath, GitContext gitContext) { if (repositoryPath == null || !Files.isDirectory(repositoryPath)) { return RepositoryType.UNKNOWN; } - if (isSettingsFolder(repositoryPath) && Files.exists(repositoryPath.resolve(GitContext.GIT_FOLDER))) { + if (isSettingsFolder(repositoryPath) && gitContext.isGitRepo(repositoryPath)) { return RepositoryType.SETTINGS; } Path settingsFolder = repositoryPath.resolve(IdeContext.FOLDER_SETTINGS); From a01d6c75510bdae33c3f611bf81cc2ccc87703c0 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Thu, 3 Sep 2026 15:58:21 +0200 Subject: [PATCH 84/89] #1695: - corrected documentation - fixed wrong path in checkSettingsPresent - renamed HealthCheckResultStatus.java to SettingsHealthCheckResult Signed-off-by: laim2003 --- .../commandlet/update/AbstractUpdateCommandlet.java | 9 +++++---- .../update/settings/SettingsHealthCheckResult.java | 11 ++++++----- ...sultStatus.java => SettingsHealthCheckStatus.java} | 2 +- .../commandlet/update/settings/SettingsUpdater.java | 8 +++++--- .../tools/ide/git/repository/RepositoryUtil.java | 7 +++++++ documentation/settings.adoc | 3 --- 6 files changed, 24 insertions(+), 16 deletions(-) rename cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/{HealthCheckResultStatus.java => SettingsHealthCheckStatus.java} (92%) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index c67559d0d7..9ae4871c4d 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -16,8 +16,8 @@ import com.devonfw.tools.ide.commandlet.Commandlet; import com.devonfw.tools.ide.commandlet.CommandletManager; import com.devonfw.tools.ide.commandlet.CreateCommandlet; -import com.devonfw.tools.ide.commandlet.update.settings.HealthCheckResultStatus; import com.devonfw.tools.ide.commandlet.update.settings.SettingsHealthCheckResult; +import com.devonfw.tools.ide.commandlet.update.settings.SettingsHealthCheckStatus; import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdateResult; import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdater; import com.devonfw.tools.ide.context.AbstractIdeContext; @@ -57,6 +57,7 @@ public abstract class AbstractUpdateCommandlet extends Commandlet { /** {@link FlagProperty} for skipping the setup of git repositories. */ public final FlagProperty skipRepositories; + //TODO: If this is only used for the case of code-settings repos, why have that property here and not in UpdateCommandlet? /** {@link FlagProperty} to force the update of the settings git repository. */ public final FlagProperty forcePull; @@ -189,11 +190,11 @@ private void updateSettingsInStep() { SettingsHealthCheckResult healthCheckResult; healthCheckResult = healthCheckStep.call(() -> { SettingsHealthCheckResult _healthCheckResult = settingsUpdater.checkSettings(this.context.getSettingsPath()); - HealthCheckResultStatus status = _healthCheckResult.status(); + SettingsHealthCheckStatus status = _healthCheckResult.status(); if (status == null) { throw new CliException("Health check on settings failed due to unknown error - the settings have not been updated"); - } else if (status == HealthCheckResultStatus.SETTINGS_INVALID) { + } else if (status == SettingsHealthCheckStatus.SETTINGS_INVALID) { throw new CliException("The settings health check failed: " + _healthCheckResult.errorMessage()); } return _healthCheckResult; @@ -213,7 +214,7 @@ private void updateSettingsInStep() { Step applySettingsStep = this.context.newStep("Applying settings"); applySettingsStep.run(() -> { - boolean onlyPull = healthCheckResult.status() == HealthCheckResultStatus.SETTINGS_VALID && healthCheckResult.isExistingProject(); + boolean onlyPull = healthCheckResult.status() == SettingsHealthCheckStatus.SETTINGS_VALID && healthCheckResult.isExistingProject(); SettingsUpdateResult settingsUpdateResult = settingsUpdater.applySettings(onlyPull, healthCheckResult.temporarySettingsDirectory()); if (settingsUpdateResult == null) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java index 3fe59a3023..973f3c99e3 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java @@ -7,21 +7,21 @@ /** * Result of the settings {@link SettingsUpdater#checkSettings(Path)} health check}. * - * @param status the {@link HealthCheckResultStatus}. + * @param status the {@link SettingsHealthCheckStatus}. * @param repositoryType the {@link RepositoryType} of the settings repository. * @param errorMessage the reason why the settings could not be updated or {@code null} if the health check succeeded. * @param temporarySettingsDirectory path to the temporary folder this health check was performed on. */ -public record SettingsHealthCheckResult(HealthCheckResultStatus status, RepositoryType repositoryType, Path temporarySettingsDirectory, String errorMessage, +public record SettingsHealthCheckResult(SettingsHealthCheckStatus status, RepositoryType repositoryType, Path temporarySettingsDirectory, String errorMessage, boolean isExistingProject) { /** - * @param status the {@link HealthCheckResultStatus}. + * @param status the {@link SettingsHealthCheckStatus}. * @param repositoryType the {@link RepositoryType}. * @param temporarySettingsDirectory path to the temporary folder this health check was performed on. * @return a {@link SettingsHealthCheckResult} for a successful health check. */ - public static SettingsHealthCheckResult of(HealthCheckResultStatus status, RepositoryType repositoryType, Path temporarySettingsDirectory, + public static SettingsHealthCheckResult of(SettingsHealthCheckStatus status, RepositoryType repositoryType, Path temporarySettingsDirectory, boolean isExistingProject) { return new SettingsHealthCheckResult(status, repositoryType, temporarySettingsDirectory, null, isExistingProject); @@ -36,6 +36,7 @@ public static SettingsHealthCheckResult of(HealthCheckResultStatus status, Repos public static SettingsHealthCheckResult failed(RepositoryType repositoryType, String errorMessage, Path temporarySettingsDirectory, boolean isExistingProject) { - return new SettingsHealthCheckResult(HealthCheckResultStatus.SETTINGS_INVALID, repositoryType, temporarySettingsDirectory, errorMessage, isExistingProject); + return new SettingsHealthCheckResult(SettingsHealthCheckStatus.SETTINGS_INVALID, repositoryType, temporarySettingsDirectory, errorMessage, + isExistingProject); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckStatus.java similarity index 92% rename from cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java rename to cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckStatus.java index 5b6c9f45be..1ab0280cfb 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/HealthCheckResultStatus.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckStatus.java @@ -6,7 +6,7 @@ * Status of the settings {@link SettingsUpdater#checkSettings(Path)} health check} describing what {@link SettingsUpdater#applySettings(boolean, Path)} has to * do. */ -public enum HealthCheckResultStatus { +public enum SettingsHealthCheckStatus { /** The settings repository was cloned to a temporary directory and is valid - it can be moved to its final location. */ SETTINGS_VALID, /** The settings repository is invalid */ diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index 50d21c9296..41da4ba6de 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -97,7 +97,8 @@ private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, Reposi try { //Get Git url of existing settings, clone newest version of them to temp dir GitUrl gitUrl = GitUrl.of(this.context.getGitContext().retrieveGitUrl(settingsPath)); - RepositoryType clonedType = RepositoryUtil.getRepositoryType(cloneRepoToTempDir(gitUrl), this.context.getGitContext()); + Path tempDir = cloneRepoToTempDir(gitUrl); + RepositoryType clonedType = RepositoryUtil.getRepositoryType(tempDir, this.context.getGitContext()); cleanup(); //If cloned repo is not (code-)settings repo and no force override (e.g. force mode) is applied, return error. @@ -106,7 +107,7 @@ private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, Reposi } //Otherwise, (e.g. user overrides), return valid. - return SettingsHealthCheckResult.of(HealthCheckResultStatus.SETTINGS_VALID, repositoryType, settingsPath, true); + return SettingsHealthCheckResult.of(SettingsHealthCheckStatus.SETTINGS_VALID, repositoryType, tempDir, true); } catch (RuntimeException e) { cleanup(); if (e instanceof CliAbortException) { @@ -134,7 +135,7 @@ private SettingsHealthCheckResult checkClonedSettings(Path settingsPath) { //see @javadoc why we throw fatally here. return SettingsHealthCheckResult.failed(repositoryType, MESSAGE_INVALID_REPOSITORY, tempCloneDir, false); } - return SettingsHealthCheckResult.of(HealthCheckResultStatus.SETTINGS_VALID, repositoryType, tempCloneDir, false); + return SettingsHealthCheckResult.of(SettingsHealthCheckStatus.SETTINGS_VALID, repositoryType, tempCloneDir, false); } catch (RuntimeException e) { cleanup(); throw createGuaranteedFatalException(e); @@ -147,6 +148,7 @@ private SettingsHealthCheckResult checkClonedSettings(Path settingsPath) { * {@link CliException#getExitCode() exit code} so that e.g. an abort by the user is still reported as such. */ private static CliFatalException createGuaranteedFatalException(RuntimeException error) { + //TODO: Dont drop the exit code here if (error instanceof CliFatalException rethrow) { return rethrow; diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java index 0bafb2a19a..00e33d5110 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java @@ -25,6 +25,13 @@ public static RepositoryType getRepositoryType(Path repositoryPath, GitContext g return RepositoryType.UNKNOWN; } if (isSettingsFolder(repositoryPath) && gitContext.isGitRepo(repositoryPath)) { + //TODO: review this for the case of code-settings repo. This could cause issues acc. to claude: + // Combined code-settings repo + --force/--force-pull regression (source-trace-confirmed, niche path — please author-confirm). + // For a combined repo (IDE_HOME/settings → symlink into /settings, .git one level up), + // getRepositoryType(IDE_HOME/settings) classifies as PLAIN_CODE because .git isn't in that folder (RepositoryUtil.java:22-39). + // The non-force case is safe (the guard at :170 skips the pull), but --force/--force-pull bypasses the guard and then checkClonedSettings + // backs up the valid settings and re-clones from scratch instead of pulling; even a passing check would then fail in + // applySettings (which re-derives PLAIN_CODE at :229). On main this path was a plain git pull. No test covers it. return RepositoryType.SETTINGS; } Path settingsFolder = repositoryPath.resolve(IdeContext.FOLDER_SETTINGS); diff --git a/documentation/settings.adoc b/documentation/settings.adoc index 3bf2f8e240..8c79f684e9 100644 --- a/documentation/settings.adoc +++ b/documentation/settings.adoc @@ -63,9 +63,6 @@ Only if this health check succeeded the settings are installed: an existing sett This way a broken or wrong git URL can never leave you with a damaged project. In particular `ide create` will not create the project at all if the health check fails, so you can simply fix the URL and try again. -If you are sure that you know better, you can use the `--force` option. -`IDEasy` will then still report the problem but ask you whether you want to continue anyway. - == Structure The settings folder has to follow this file structure: From 9a8f8414cd076adac0fde469349df398dadf8171 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 4 Sep 2026 13:42:45 +0200 Subject: [PATCH 85/89] #1695: - merged RepositoryUtil.java into RepositoryType - updated tests Signed-off-by: laim2003 --- .../update/AbstractUpdateCommandlet.java | 5 +- .../update/settings/SettingsUpdater.java | 11 ++-- .../ide/git/repository/RepositoryType.java | 51 +++++++++++++++++ .../ide/git/repository/RepositoryUtil.java | 57 ------------------- .../ide/commandlet/CreateCommandletTest.java | 24 -------- 5 files changed, 59 insertions(+), 89 deletions(-) delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 9ae4871c4d..382d90a7a1 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -13,6 +13,7 @@ import org.slf4j.LoggerFactory; import com.devonfw.tools.ide.cli.CliException; +import com.devonfw.tools.ide.cli.CliFatalException; import com.devonfw.tools.ide.commandlet.Commandlet; import com.devonfw.tools.ide.commandlet.CommandletManager; import com.devonfw.tools.ide.commandlet.CreateCommandlet; @@ -192,8 +193,8 @@ private void updateSettingsInStep() { SettingsHealthCheckResult _healthCheckResult = settingsUpdater.checkSettings(this.context.getSettingsPath()); SettingsHealthCheckStatus status = _healthCheckResult.status(); - if (status == null) { - throw new CliException("Health check on settings failed due to unknown error - the settings have not been updated"); + if ((status == null || status == SettingsHealthCheckStatus.SETTINGS_INVALID) && !_healthCheckResult.isExistingProject()) { + throw new CliFatalException("Fatal error while cloning settings: The settings health check failed: " + _healthCheckResult.errorMessage()); } else if (status == SettingsHealthCheckStatus.SETTINGS_INVALID) { throw new CliException("The settings health check failed: " + _healthCheckResult.errorMessage()); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index 41da4ba6de..8b459abbfe 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -13,7 +13,6 @@ import com.devonfw.tools.ide.git.GitContext; import com.devonfw.tools.ide.git.GitUrl; import com.devonfw.tools.ide.git.repository.RepositoryType; -import com.devonfw.tools.ide.git.repository.RepositoryUtil; import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.property.StringProperty; @@ -79,7 +78,7 @@ public SettingsHealthCheckResult checkSettings(Path settingsPath) { if (settingsPath != null && !fileAccess.isEmptyDir(settingsPath)) { // for a combined code and settings repository IDE_HOME/settings is a symlink into the code repository whose '.git' folder is one level above, // so isGitRepo would report it as broken settings - RepositoryType settingsRepoType = RepositoryUtil.getRepositoryType(settingsPath, this.context.getGitContext()); + RepositoryType settingsRepoType = RepositoryType.of(settingsPath, this.context.getGitContext()); if (settingsRepoType.isSettingsOrCodeSettingsRepository()) { return checkSettingsPresent(settingsPath, settingsRepoType); } @@ -98,7 +97,7 @@ private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, Reposi //Get Git url of existing settings, clone newest version of them to temp dir GitUrl gitUrl = GitUrl.of(this.context.getGitContext().retrieveGitUrl(settingsPath)); Path tempDir = cloneRepoToTempDir(gitUrl); - RepositoryType clonedType = RepositoryUtil.getRepositoryType(tempDir, this.context.getGitContext()); + RepositoryType clonedType = RepositoryType.of(tempDir, this.context.getGitContext()); cleanup(); //If cloned repo is not (code-)settings repo and no force override (e.g. force mode) is applied, return error. @@ -129,7 +128,7 @@ private SettingsHealthCheckResult checkClonedSettings(Path settingsPath) { GitUrl gitUrl = getOrAskSettingsUrl(); Path tempCloneDir = cloneRepoToTempDir(gitUrl); - RepositoryType repositoryType = RepositoryUtil.getRepositoryType(tempCloneDir, this.context.getGitContext()); + RepositoryType repositoryType = RepositoryType.of(tempCloneDir, this.context.getGitContext()); if (!repositoryType.isSettingsOrCodeSettingsRepository()) { //see @javadoc why we throw fatally here. @@ -223,12 +222,12 @@ private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryTyp public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { GitContext gitContext = this.context.getGitContext(); - RepositoryType repositoryType = RepositoryUtil.getRepositoryType(sourcePath, gitContext); + RepositoryType repositoryType = RepositoryType.of(sourcePath, gitContext); Path settingsPath = this.context.getSettingsPath(); // Case 1: We performed "ide update"; so settings already existed and we just need to perform a git pull in the existing repo. if (onlyPull) { - repositoryType = RepositoryUtil.getRepositoryType(context.getSettingsPath(), gitContext); + repositoryType = RepositoryType.of(context.getSettingsPath(), gitContext); if (repositoryType != RepositoryType.SETTINGS) { return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java index b6d27fbac6..ea2b8eaf51 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java @@ -1,5 +1,12 @@ package com.devonfw.tools.ide.git.repository; +import java.nio.file.Files; +import java.nio.file.Path; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.environment.EnvironmentVariables; +import com.devonfw.tools.ide.git.GitContext; + /** * Enum representation of a detected {@link RepositoryType}. */ @@ -17,10 +24,54 @@ public enum RepositoryType { /** The type of the repository could not be determined. */ UNKNOWN; + /** + * Checks whether the given git repository is a settings repository, a combined settings and code repository, or a typical code repository. A combined code + * and settings repository is detected by a top-level {@code settings} folder that itself is a valid settings folder. + * + * @param repositoryPath the {@link Path} to the repository to check. + * @param gitContext a {@link GitContext} + * @return the {@link RepositoryType} of the repository. + */ + public static RepositoryType of(Path repositoryPath, GitContext gitContext) { + + if (repositoryPath == null || !Files.isDirectory(repositoryPath)) { + return RepositoryType.UNKNOWN; + } + if (isSettingsFolder(repositoryPath) && gitContext.isGitRepo(repositoryPath)) { + //TODO: review this for the case of code-settings repo. This could cause issues acc. to claude: + // Combined code-settings repo + --force/--force-pull regression (source-trace-confirmed, niche path — please author-confirm). + // For a combined repo (IDE_HOME/settings → symlink into /settings, .git one level up), + // getRepositoryType(IDE_HOME/settings) classifies as PLAIN_CODE because .git isn't in that folder (RepositoryUtil.java:22-39). + // The non-force case is safe (the guard at :170 skips the pull), but --force/--force-pull bypasses the guard and then checkClonedSettings + // backs up the valid settings and re-clones from scratch instead of pulling; even a passing check would then fail in + // applySettings (which re-derives PLAIN_CODE at :229). On main this path was a plain git pull. No test covers it. + return RepositoryType.SETTINGS; + } + Path settingsFolder = repositoryPath.resolve(IdeContext.FOLDER_SETTINGS); + if (isSettingsFolder(settingsFolder)) { + return RepositoryType.CODE_SETTINGS_COMBINED; + } + if (!Files.exists(settingsFolder)) { + return RepositoryType.PLAIN_CODE; + } + // there is no valid settings folder to be found. + return RepositoryType.UNKNOWN; + } + /** * @return true if repository is either of type {@code SETTINGS} or {@code CODE_SETTINGS_COMBINED} */ public boolean isSettingsOrCodeSettingsRepository() { return this == SETTINGS || this == CODE_SETTINGS_COMBINED; } + + /** + * @param folder the {@link Path} to check. + * @return {@code true} if the given {@code folder} is the root of a settings repository, {@code false} otherwise. + */ + private static boolean isSettingsFolder(Path folder) { + + return (Files.exists(folder.resolve(EnvironmentVariables.DEFAULT_PROPERTIES)) + || Files.exists(folder.resolve(EnvironmentVariables.LEGACY_PROPERTIES))); + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java deleted file mode 100644 index 00e33d5110..0000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryUtil.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.devonfw.tools.ide.git.repository; - -import java.nio.file.Files; -import java.nio.file.Path; - -import com.devonfw.tools.ide.context.IdeContext; -import com.devonfw.tools.ide.environment.EnvironmentVariables; -import com.devonfw.tools.ide.git.GitContext; - -/** - * Utility class for IDEasy settings/code repositories. - */ -public class RepositoryUtil { - - /** - * Checks whether the given git repository is a settings repository, a combined settings and code repository, or a typical code repository. A combined code - * and settings repository is detected by a top-level {@code settings} folder that itself is a valid settings folder. - * - * @param repositoryPath the {@link Path} to the repository to check. - * @return the {@link RepositoryType} of the repository. - */ - public static RepositoryType getRepositoryType(Path repositoryPath, GitContext gitContext) { - - if (repositoryPath == null || !Files.isDirectory(repositoryPath)) { - return RepositoryType.UNKNOWN; - } - if (isSettingsFolder(repositoryPath) && gitContext.isGitRepo(repositoryPath)) { - //TODO: review this for the case of code-settings repo. This could cause issues acc. to claude: - // Combined code-settings repo + --force/--force-pull regression (source-trace-confirmed, niche path — please author-confirm). - // For a combined repo (IDE_HOME/settings → symlink into /settings, .git one level up), - // getRepositoryType(IDE_HOME/settings) classifies as PLAIN_CODE because .git isn't in that folder (RepositoryUtil.java:22-39). - // The non-force case is safe (the guard at :170 skips the pull), but --force/--force-pull bypasses the guard and then checkClonedSettings - // backs up the valid settings and re-clones from scratch instead of pulling; even a passing check would then fail in - // applySettings (which re-derives PLAIN_CODE at :229). On main this path was a plain git pull. No test covers it. - return RepositoryType.SETTINGS; - } - Path settingsFolder = repositoryPath.resolve(IdeContext.FOLDER_SETTINGS); - if (isSettingsFolder(settingsFolder)) { - return RepositoryType.CODE_SETTINGS_COMBINED; - } - if (!Files.exists(settingsFolder)) { - return RepositoryType.PLAIN_CODE; - } - // there is no valid settings folder to be found. - return RepositoryType.UNKNOWN; - } - - /** - * @param folder the {@link Path} to check. - * @return {@code true} if the given {@code folder} is the root of a settings repository, {@code false} otherwise. - */ - private static boolean isSettingsFolder(Path folder) { - - return (Files.exists(folder.resolve(EnvironmentVariables.DEFAULT_PROPERTIES)) - || Files.exists(folder.resolve(EnvironmentVariables.LEGACY_PROPERTIES))); - } -} diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java index 56e6f403cb..ab77c1b79d 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java @@ -222,30 +222,6 @@ void testCreateWithCodeSettingsRepository() { assertThat(settingsLink.resolve("ide.properties")).exists(); } - @Test - void testCreateWithInvalidRepositoryContinuesInForceMode() { - - // arrange - force mode lets the user decide to continue even though the health check failed - GitContextImplMock gitContextImplMock = new GitContextImplMock(context, TEST_RESOURCES.resolve("pypi")); - context.setGitContext(gitContextImplMock); - context.getStartContext().setForceMode(true); - context.setAnswers("yes"); - CreateCommandlet cc = context.getCommandletManager().getCommandlet(CreateCommandlet.class); - cc.newProject.setValueAsString(NEW_PROJECT_NAME, context); - cc.settingsRepo.setValue(IdeContext.DEFAULT_SETTINGS_REPO_URL); - cc.skipTools.setValue(true); - cc.skipRepositories.setValue(true); - - // act - cc.run(); - - // assert - Path newProjectPath = context.getIdeRoot().resolve(NEW_PROJECT_NAME); - assertThat(newProjectPath).exists(); - assertThat(context).logAtWarning() - .hasMessageContaining("does not point to a valid settings or code-settings repository"); - } - @Test void testCreateWithDashPlaceholderAsCliArgument() { // arrange - see https://github.com/devonfw/IDEasy/issues/2106 From abdd81846da468b798cb60d3b89b53fa816b712b Mon Sep 17 00:00:00 2001 From: laim2003 Date: Fri, 4 Sep 2026 15:54:19 +0200 Subject: [PATCH 86/89] #1695: - updated error message - fixed test Signed-off-by: laim2003 --- .../ide/commandlet/update/settings/SettingsUpdater.java | 6 +++--- .../devonfw/tools/ide/commandlet/CreateCommandletTest.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index 8b459abbfe..61e6431168 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -36,8 +36,8 @@ public class SettingsUpdater { Please contact the technical lead of your project to get the SETTINGS_URL for your project to enter. In case you just want to test IDEasy you may simply hit return to install the default settings."""; - private static final String MESSAGE_INVALID_REPOSITORY = "Settings repository integrity check failed: " - + "The given git repository URL does not point to a valid settings or code-settings repository. Please verify and try again."; + private static final String MESSAGE_INVALID_REPOSITORY = "The given git repository URL does not point to a valid settings or code-settings repository. " + + "Please verify and try again."; private final IdeContext context; @@ -152,7 +152,7 @@ private static CliFatalException createGuaranteedFatalException(RuntimeException if (error instanceof CliFatalException rethrow) { return rethrow; } else if (error instanceof CliException) { - return new CliFatalException(error.getMessage(), error); + return new CliFatalException("Error occurred during settings update: " + error.getMessage(), error); } return new CliFatalException("Error occurred during settings update: " + error.getClass() + ": " + error.getMessage(), error); } diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java index ab77c1b79d..395b0957b5 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CreateCommandletTest.java @@ -189,7 +189,7 @@ void testProjectWithInvalidRepositoryNotCreated() { assertThatThrownBy(cc::run) .isInstanceOf(CliException.class) .hasMessageContaining( - "Settings repository integrity check failed: " + "Fatal error while cloning settings: The settings health check failed: " + "The given git repository URL does not point to a valid settings or code-settings repository. Please verify and try again."); // assert - if "ide create" fails then no project shall be created at all From 8215bb69162efb601a7a35a5fbab44d1be6ee622 Mon Sep 17 00:00:00 2001 From: laim2003 Date: Tue, 8 Sep 2026 17:09:50 +0200 Subject: [PATCH 87/89] #1695: - settings health check now always uses correct settings path depending on CreateCommandlet/UpdateCommandlet - IdeContext now uses RepositoryType's settings type determination rather than its own method. - IDE_HOME is now only set in CreateCommandlet once the settings health check has passed - applied spotless plugin - Fixed issue that in UpdateCommandlet, logic would not ask user whether he wants to clone if settings repo is broken. Signed-off-by: laim2003 --- .../ide/commandlet/CreateCommandlet.java | 14 ++- .../ide/commandlet/StatusCommandlet.java | 3 +- .../update/AbstractUpdateCommandlet.java | 77 +++++++++------- .../settings/SettingsHealthCheckResult.java | 16 ++-- .../update/settings/SettingsUpdater.java | 88 +++++++++++-------- .../tools/ide/context/AbstractIdeContext.java | 28 ++---- .../devonfw/tools/ide/context/IdeContext.java | 8 +- .../ide/git/repository/RepositoryType.java | 57 ++++++++---- 8 files changed, 164 insertions(+), 127 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index 9bf6014420..52d81d3508 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -58,9 +58,7 @@ protected void doRun() { this.context.askToContinue("Directory {} already exists. Do you want to continue?", newProjectPath); fileAccess.backup(newProjectPath); } - // point IDE_HOME to the new project before the settings are checked - this only computes the paths and creates nothing on disk so that a failing - // health check leaves no project behind. As IDE_HOME/settings does not exist yet the settings will be cloned instead of pulled. - this.context.setIdeHome(newProjectPath); + super.doRun(); this.context.getFileAccess().writeFileContent(IdeVersion.getVersionString(), newProjectPath.resolve(IdeContext.FILE_SOFTWARE_VERSION)); IdeLogLevel.SUCCESS.log(LOG, "Successfully created new project '{}'.", this.newProject.getValue()); @@ -72,6 +70,7 @@ protected void onSettingHealthCheckFinished() { // only called after the settings passed the health check Path newProjectPath = getNewProjectPath(); + this.context.setIdeHome(newProjectPath); FileAccess fileAccess = this.context.getFileAccess(); fileAccess.mkdirs(newProjectPath); fileAccess.mkdirs(newProjectPath.resolve(IdeContext.FOLDER_SOFTWARE)); @@ -79,6 +78,15 @@ protected void onSettingHealthCheckFinished() { fileAccess.mkdirs(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)); } + /** + * @return The path to the settings folder in the new project. + */ + @Override + protected Path getSettingsPathForSettingsUpdate() { + + return getNewProjectPath().resolve(IdeContext.FOLDER_SETTINGS); + } + private Path getNewProjectPath() { return this.context.getIdeRoot().resolve(this.newProject.getValue()); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java index dab3c83d5d..71fe06e024 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java @@ -9,6 +9,7 @@ import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.environment.EnvironmentVariables; import com.devonfw.tools.ide.git.GitContext; +import com.devonfw.tools.ide.git.repository.RepositoryType; import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.migration.IdeMigrator; import com.devonfw.tools.ide.os.SystemInfo; @@ -105,7 +106,7 @@ private void logSettingsGitStatus() { } else { GitContext gitContext = this.context.getGitContext(); if (gitContext.isRepositoryUpdateAvailable(settingsPath, this.context.getSettingsCommitIdPath())) { - if (!this.context.isCombinedSettingsCodeRepository()) { + if (RepositoryType.ofSettingsPath(this.context.getSettingsPath(), context) != RepositoryType.CODE_SETTINGS_COMBINED) { LOG.warn("Your settings are not up-to-date, please run 'ide update'."); } } else { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java index 382d90a7a1..c0b54b1554 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/AbstractUpdateCommandlet.java @@ -6,6 +6,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.stream.Stream; @@ -18,13 +19,14 @@ import com.devonfw.tools.ide.commandlet.CommandletManager; import com.devonfw.tools.ide.commandlet.CreateCommandlet; import com.devonfw.tools.ide.commandlet.update.settings.SettingsHealthCheckResult; -import com.devonfw.tools.ide.commandlet.update.settings.SettingsHealthCheckStatus; import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdateResult; +import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdateStatus; import com.devonfw.tools.ide.commandlet.update.settings.SettingsUpdater; import com.devonfw.tools.ide.context.AbstractIdeContext; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.context.IdeStartContextImpl; import com.devonfw.tools.ide.git.repository.RepositoryCommandlet; +import com.devonfw.tools.ide.git.repository.RepositoryType; import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.property.FlagProperty; import com.devonfw.tools.ide.property.StringProperty; @@ -168,8 +170,9 @@ private void setupConf(Path template, Path conf) { */ protected void updateSettings() { - boolean codeRepository = this.context.isCombinedSettingsCodeRepository(); - if (codeRepository && !(this.context.isForceMode() || this.forcePull.isTrue())) { + //TODO: replace getSettingsPath in CreateCommandlet + RepositoryType repositoryType = RepositoryType.ofSettingsPath(getSettingsPathForSettingsUpdate(), context); + if (repositoryType == RepositoryType.CODE_SETTINGS_COMBINED && !(this.context.isForceMode() || this.forcePull.isTrue())) { LOG.info("Skipping git pull in settings due to code repository. Use --force-pull to enforce pulling."); return; } @@ -188,18 +191,7 @@ private void updateSettingsInStep() { try { //Step 1: Perform health check Step healthCheckStep = this.context.newStep("Performing settings health check"); - SettingsHealthCheckResult healthCheckResult; - healthCheckResult = healthCheckStep.call(() -> { - SettingsHealthCheckResult _healthCheckResult = settingsUpdater.checkSettings(this.context.getSettingsPath()); - SettingsHealthCheckStatus status = _healthCheckResult.status(); - - if ((status == null || status == SettingsHealthCheckStatus.SETTINGS_INVALID) && !_healthCheckResult.isExistingProject()) { - throw new CliFatalException("Fatal error while cloning settings: The settings health check failed: " + _healthCheckResult.errorMessage()); - } else if (status == SettingsHealthCheckStatus.SETTINGS_INVALID) { - throw new CliException("The settings health check failed: " + _healthCheckResult.errorMessage()); - } - return _healthCheckResult; - }, () -> null); + SettingsHealthCheckResult healthCheckResult = healthCheckStep.call(() -> checkSettingsInStep(settingsUpdater), () -> null); // If the health check failed (healthCheckResult is null) the settings have not been verified, so skip applying them and fail the "Update settings" // step. A non-null result is only produced when the health check passed or the user explicitly chose to continue anyway (force mode), so this never @@ -213,22 +205,7 @@ private void updateSettingsInStep() { //Step 3: Apply (move/pull newest version) settings Step applySettingsStep = this.context.newStep("Applying settings"); - applySettingsStep.run(() -> { - - boolean onlyPull = healthCheckResult.status() == SettingsHealthCheckStatus.SETTINGS_VALID && healthCheckResult.isExistingProject(); - SettingsUpdateResult settingsUpdateResult = settingsUpdater.applySettings(onlyPull, - healthCheckResult.temporarySettingsDirectory()); - if (settingsUpdateResult == null) { - - throw new CliException("Failed to apply the settings update due to unknown error."); - } - - switch (settingsUpdateResult.updateStatus()) { - case SETTINGS_UPDATED -> applySettingsStep.success("Settings update successfully applied"); - case SETTINGS_CLONED -> applySettingsStep.success("Settings successfully applied (cloned)"); - case SETTINGS_UPDATE_FAILED -> throw new CliException("The settings update could not be applied: " + settingsUpdateResult.errorMessage()); - } - }); + applySettingsStep.run(() -> applySettingsUpdateInStep(settingsUpdater, healthCheckResult)); //Make sure to always fail the parent step if the "Apply settings" step fails. if (applySettingsStep.isFailure()) { @@ -240,6 +217,34 @@ private void updateSettingsInStep() { } } + private SettingsHealthCheckResult checkSettingsInStep(SettingsUpdater settingsUpdater) { + SettingsHealthCheckResult healthCheckResult = settingsUpdater.checkSettings(getSettingsPathForSettingsUpdate()); + RepositoryType repositoryType = healthCheckResult.repositoryType(); + + if ((repositoryType == null || !repositoryType.isValid()) && !healthCheckResult.isExistingProject()) { + throw new CliFatalException("Fatal error while cloning settings: The settings health check failed: " + healthCheckResult.errorMessage()); + } else if ((repositoryType == null || !repositoryType.isValid())) { + throw new CliException("The settings health check failed: " + healthCheckResult.errorMessage()); + } + return healthCheckResult; + } + + private void applySettingsUpdateInStep(SettingsUpdater settingsUpdater, SettingsHealthCheckResult healthCheckResult) { + SettingsUpdateResult settingsUpdateResult = settingsUpdater.applySettings(healthCheckResult); + if (settingsUpdateResult == null) { + + throw new CliException("Failed to apply the settings update due to unknown error."); + } + + if (Objects.requireNonNull(settingsUpdateResult.updateStatus()) == SettingsUpdateStatus.SETTINGS_UPDATE_FAILED) { + String errorMessage = "The settings update could not be applied: " + settingsUpdateResult.errorMessage(); + if (!healthCheckResult.isExistingProject()) { + throw new CliFatalException(errorMessage); + } + throw new CliException(errorMessage); + } + } + private void updateSoftware() { if (this.skipTools.isTrue()) { @@ -395,4 +400,14 @@ private void createStartScript(String ide, String workspace) { fileAccess.writeFileContent(scriptContent, scriptPath); fileAccess.makeExecutable(scriptPath); } + + /** + * This method returns the path to the settings for the case of a settings update. + * + * @return The {@link Path} to the settings folder; if not overridden we will get the path from the context + */ + protected Path getSettingsPathForSettingsUpdate() { + + return this.context.getSettingsPath(); + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java index 973f3c99e3..41dd6d9ec4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsHealthCheckResult.java @@ -7,36 +7,36 @@ /** * Result of the settings {@link SettingsUpdater#checkSettings(Path)} health check}. * - * @param status the {@link SettingsHealthCheckStatus}. * @param repositoryType the {@link RepositoryType} of the settings repository. * @param errorMessage the reason why the settings could not be updated or {@code null} if the health check succeeded. - * @param temporarySettingsDirectory path to the temporary folder this health check was performed on. + * @param settingsDirectory path to the temporary folder this health check was performed on. */ -public record SettingsHealthCheckResult(SettingsHealthCheckStatus status, RepositoryType repositoryType, Path temporarySettingsDirectory, String errorMessage, +public record SettingsHealthCheckResult(RepositoryType repositoryType, Path settingsDirectory, String errorMessage, boolean isExistingProject) { /** - * @param status the {@link SettingsHealthCheckStatus}. * @param repositoryType the {@link RepositoryType}. * @param temporarySettingsDirectory path to the temporary folder this health check was performed on. + * @param isExistingProject whether the settings we checked were freshly cloned or already existed * @return a {@link SettingsHealthCheckResult} for a successful health check. */ - public static SettingsHealthCheckResult of(SettingsHealthCheckStatus status, RepositoryType repositoryType, Path temporarySettingsDirectory, + public static SettingsHealthCheckResult ofSuccess(RepositoryType repositoryType, Path temporarySettingsDirectory, boolean isExistingProject) { - return new SettingsHealthCheckResult(status, repositoryType, temporarySettingsDirectory, null, isExistingProject); + return new SettingsHealthCheckResult(repositoryType, temporarySettingsDirectory, null, isExistingProject); } /** * @param repositoryType the {@link RepositoryType} of the settings that are already present. * @param errorMessage the reason why the settings could not be updated. * @param temporarySettingsDirectory path to the temporary folder this health check was performed on. + * @param isExistingProject whether the settings we checked were cloned freshly or already existed * @return a {@link SettingsHealthCheckResult} for a failed but recoverable health check. */ - public static SettingsHealthCheckResult failed(RepositoryType repositoryType, String errorMessage, Path temporarySettingsDirectory, + public static SettingsHealthCheckResult ofFailed(RepositoryType repositoryType, String errorMessage, Path temporarySettingsDirectory, boolean isExistingProject) { - return new SettingsHealthCheckResult(SettingsHealthCheckStatus.SETTINGS_INVALID, repositoryType, temporarySettingsDirectory, errorMessage, + return new SettingsHealthCheckResult(repositoryType, temporarySettingsDirectory, errorMessage, isExistingProject); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index 61e6431168..fb956d7c0d 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -78,12 +78,9 @@ public SettingsHealthCheckResult checkSettings(Path settingsPath) { if (settingsPath != null && !fileAccess.isEmptyDir(settingsPath)) { // for a combined code and settings repository IDE_HOME/settings is a symlink into the code repository whose '.git' folder is one level above, // so isGitRepo would report it as broken settings - RepositoryType settingsRepoType = RepositoryType.of(settingsPath, this.context.getGitContext()); - if (settingsRepoType.isSettingsOrCodeSettingsRepository()) { - return checkSettingsPresent(settingsPath, settingsRepoType); - } + return checkSettingsPresent(settingsPath); } - return checkClonedSettings(settingsPath); + return cloneAndCheckSettings(settingsPath); } /** @@ -91,29 +88,25 @@ public SettingsHealthCheckResult checkSettings(Path settingsPath) { * aborted. Here, if a new version is available, we clone the new version into a temporary folder and perform health checks. If the cloned, new version is * valid, we call git update in the existing settings folder. */ - private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, RepositoryType repositoryType) { + private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath) { + RepositoryType settingsRepoType = RepositoryType.ofSettingsPath(settingsPath, this.context); try { - //Get Git url of existing settings, clone newest version of them to temp dir - GitUrl gitUrl = GitUrl.of(this.context.getGitContext().retrieveGitUrl(settingsPath)); - Path tempDir = cloneRepoToTempDir(gitUrl); - RepositoryType clonedType = RepositoryType.of(tempDir, this.context.getGitContext()); - cleanup(); - //If cloned repo is not (code-)settings repo and no force override (e.g. force mode) is applied, return error. - if (!clonedType.isSettingsOrCodeSettingsRepository() && !requestUserConfirmInvalidRepository(clonedType, gitUrl)) { - return SettingsHealthCheckResult.failed(clonedType, MESSAGE_INVALID_REPOSITORY, settingsPath, true); + //If cloned repo is not (code-)settings repo, let the user re-clone the settings. + if (!settingsRepoType.isValid()) { + return cloneAndCheckSettings(settingsPath); } //Otherwise, (e.g. user overrides), return valid. - return SettingsHealthCheckResult.of(SettingsHealthCheckStatus.SETTINGS_VALID, repositoryType, tempDir, true); + return SettingsHealthCheckResult.ofSuccess(settingsRepoType, settingsPath, true); } catch (RuntimeException e) { cleanup(); if (e instanceof CliAbortException) { // the user answered "no" so we must not silently carry on - return SettingsHealthCheckResult.failed(repositoryType, "Settings update aborted by end-user", settingsPath, true); + return SettingsHealthCheckResult.ofFailed(settingsRepoType, "Settings update aborted by end-user", settingsPath, true); } - return SettingsHealthCheckResult.failed(repositoryType, e.getMessage(), settingsPath, true); + return SettingsHealthCheckResult.ofFailed(settingsRepoType, e.getMessage(), settingsPath, true); } } @@ -121,20 +114,22 @@ private SettingsHealthCheckResult checkSettingsPresent(Path settingsPath, Reposi * Health check for missing or broken settings (e.g. {@code ide create}). Without valid settings there is nothing to continue with, so every failure is fatal * here. */ - private SettingsHealthCheckResult checkClonedSettings(Path settingsPath) { + private SettingsHealthCheckResult cloneAndCheckSettings(Path settingsPath) { try { - backupBrokenSettings(settingsPath); + if (settingsPath != null) { + backupBrokenSettings(settingsPath); + } GitUrl gitUrl = getOrAskSettingsUrl(); Path tempCloneDir = cloneRepoToTempDir(gitUrl); - RepositoryType repositoryType = RepositoryType.of(tempCloneDir, this.context.getGitContext()); + RepositoryType repositoryType = RepositoryType.ofGitRoot(tempCloneDir, this.context); - if (!repositoryType.isSettingsOrCodeSettingsRepository()) { + if (!repositoryType.isValid()) { //see @javadoc why we throw fatally here. - return SettingsHealthCheckResult.failed(repositoryType, MESSAGE_INVALID_REPOSITORY, tempCloneDir, false); + return SettingsHealthCheckResult.ofFailed(repositoryType, MESSAGE_INVALID_REPOSITORY, tempCloneDir, false); } - return SettingsHealthCheckResult.of(SettingsHealthCheckStatus.SETTINGS_VALID, repositoryType, tempCloneDir, false); + return SettingsHealthCheckResult.ofSuccess(repositoryType, tempCloneDir, false); } catch (RuntimeException e) { cleanup(); throw createGuaranteedFatalException(e); @@ -215,19 +210,22 @@ private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryTyp * Applies the result of the {@link #checkSettings(Path)} health check by either pulling the settings in place or moving the verified clone to its final * location. * - * @param onlyPull if true, we simply perform a git pull on the actual (not the one in the temp directory) settings repository. - * @param sourcePath sourcePath of the settings to apply. + * @param healthCheckResult {@link SettingsHealthCheckResult} health check result to use for applying the settings * @return a {@link SettingsUpdateResult} representing the state, whether moving/pulling the newest settings was successful. */ - public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { + public SettingsUpdateResult applySettings(SettingsHealthCheckResult healthCheckResult) { - GitContext gitContext = this.context.getGitContext(); - RepositoryType repositoryType = RepositoryType.of(sourcePath, gitContext); + Path sourcePath = healthCheckResult.settingsDirectory(); Path settingsPath = this.context.getSettingsPath(); + RepositoryType repositoryType = RepositoryType.ofGitRoot(sourcePath, context); + + boolean onlyPull = healthCheckResult.repositoryType().isValid() && healthCheckResult.isExistingProject(); + // Case 1: We performed "ide update"; so settings already existed and we just need to perform a git pull in the existing repo. if (onlyPull) { - repositoryType = RepositoryType.of(context.getSettingsPath(), gitContext); + repositoryType = RepositoryType.ofSettingsPath(context.getSettingsPath(), this.context); + if (repositoryType != RepositoryType.SETTINGS) { return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, @@ -235,22 +233,30 @@ public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { } pullSettingsAndSaveCommitId(settingsPath); + + repositoryType = RepositoryType.ofSettingsPath(context.getSettingsPath(), this.context); + if (repositoryType != RepositoryType.SETTINGS) { + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, + repositoryType, + "The updated settings repository seems to be of an invalid type: " + repositoryType); + } + return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATED, repositoryType, null); } + String errorMessage = null; + Path gitRootDir = settingsPath; + SettingsUpdateStatus resultStatus = SettingsUpdateStatus.SETTINGS_UPDATE_FAILED; + // Case 2: We freshly cloned the settings repo and need to move it to a target directory. switch (repositoryType) { - case PLAIN_CODE, UNKNOWN -> { - - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, - "Cannot apply settings as type of the settings repo is incorrect"); - } + case UNKNOWN -> errorMessage = "Cannot apply settings as type of the settings repo is incorrect"; case SETTINGS -> { //move to IDE_HOME/SETTINGS moveProject(sourcePath, settingsPath); - this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); + + resultStatus = SettingsUpdateStatus.SETTINGS_CLONED; } case CODE_SETTINGS_COMBINED -> { @@ -264,12 +270,16 @@ public SettingsUpdateResult applySettings(boolean onlyPull, Path sourcePath) { context.getFileAccess().symlink(repoSettingsDirectory, symlinkPath); - this.context.getGitContext().saveCurrentCommitId(repoSettingsDirectory, this.context.getSettingsCommitIdPath()); - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_CLONED, repositoryType, null); + gitRootDir = repoMoveTargetDirectory; + resultStatus = SettingsUpdateStatus.SETTINGS_CLONED; } + default -> errorMessage = "Unknown error during settings"; } - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, "Unknown error during settings"); + if (errorMessage == null) { + this.context.getGitContext().saveCurrentCommitId(gitRootDir, this.context.getSettingsCommitIdPath()); + } + return new SettingsUpdateResult(resultStatus, repositoryType, errorMessage); } /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java index e0ce5e3dfa..4f514f65dc 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java @@ -50,6 +50,7 @@ import com.devonfw.tools.ide.git.GitContext; import com.devonfw.tools.ide.git.GitContextImpl; import com.devonfw.tools.ide.git.GitUrl; +import com.devonfw.tools.ide.git.repository.RepositoryType; import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.io.FileAccessImpl; import com.devonfw.tools.ide.log.IdeLogArgFormatter; @@ -699,31 +700,15 @@ public Path getSettingsPath() { public Path getSettingsGitRepository() { Path settingsPath = getSettingsPath(); + RepositoryType settingsRepositoryType = RepositoryType.ofSettingsPath(settingsPath, this); // check whether the settings path has a .git folder only if its not a symbolic link or junction - if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isCombinedSettingsCodeRepository()) { + if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !(settingsRepositoryType == RepositoryType.CODE_SETTINGS_COMBINED)) { LOG.error("Settings repository exists but is not a git repository."); return null; } return settingsPath; } - @Override - public boolean isCombinedSettingsCodeRepository() { - - Path settingsPath = getSettingsPath(); - if (settingsPath != null) { - boolean settingsIsLink = Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath); - if (settingsIsLink) { - Path realPath = getFileAccess().toRealPath(this.settingsPath); - if (realPath != null) { - return getGitContext().isGitRepo(realPath.getParent()); - } - return true; - } - } - return false; - } - @Override public Path getSettingsCommitIdPath() { @@ -1143,8 +1128,8 @@ public String askForSecret(String message, String defaultValue) { * @param message the question to ask. * @param defaultValue the value to return if the user accepts the default (by entering an empty value) or {@code null} to re-ask until a value is * entered. - * @param secret - {@code true} to read the input in a masked way (see {@link #readSecretLine()}) and to mask it in the log output, {@code false} to - * read it as plain text. + * @param secret - {@code true} to read the input in a masked way (see {@link #readSecretLine()}) and to mask it in the log output, {@code false} to read + * it as plain text. * @return the entered value or the default value. */ private String ask(String message, String defaultValue, boolean secret) { @@ -1564,7 +1549,8 @@ settingsRepository, getSettingsCommitIdPath()))) { */ private String determineSettingsUpdateMessage(Commandlet cmd) { boolean update = cmd instanceof UpdateCommandlet; - if (isCombinedSettingsCodeRepository()) { + RepositoryType settingsRepositoryType = RepositoryType.ofSettingsPath(getSettingsPath(), this); + if (settingsRepositoryType == RepositoryType.CODE_SETTINGS_COMBINED) { if (update && (isForceMode() || isForcePull())) { return null; } diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java index f36fa99d81..34f142c790 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java @@ -310,7 +310,8 @@ default String askForSecret(String message) { void addSecretVariable(String name); /** - * Registers the value of a variable as secret if the variable was marked via {@link #addSecretVariable(String)}. Has to be called before the value is logged. + * Registers the value of a variable as secret if the variable was marked via {@link #addSecretVariable(String)}. Has to be called before the value is + * logged. * * @param name the name of the variable. * @param value the value of the variable. @@ -630,11 +631,6 @@ default Path getRepositoriesPath() { */ Path getSettingsGitRepository(); - /** - * @return {@code true} if the settings repository is a symlink or a junction to a code-repository. - */ - boolean isCombinedSettingsCodeRepository(); - /** * @return the {@link Path} to the file containing the last tracked commit Id of the settings repository. */ diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java index ea2b8eaf51..7268317e74 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/repository/RepositoryType.java @@ -5,16 +5,13 @@ import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.environment.EnvironmentVariables; -import com.devonfw.tools.ide.git.GitContext; +import com.devonfw.tools.ide.io.FileAccess; /** * Enum representation of a detected {@link RepositoryType}. */ public enum RepositoryType { - /** Git Repository is a code repository. */ - PLAIN_CODE, - /** Git Repository is a settings repository. */ SETTINGS, @@ -29,39 +26,47 @@ public enum RepositoryType { * and settings repository is detected by a top-level {@code settings} folder that itself is a valid settings folder. * * @param repositoryPath the {@link Path} to the repository to check. - * @param gitContext a {@link GitContext} + * @param ideContext a {@link IdeContext} * @return the {@link RepositoryType} of the repository. */ - public static RepositoryType of(Path repositoryPath, GitContext gitContext) { + public static RepositoryType ofGitRoot(Path repositoryPath, IdeContext ideContext) { if (repositoryPath == null || !Files.isDirectory(repositoryPath)) { return RepositoryType.UNKNOWN; } - if (isSettingsFolder(repositoryPath) && gitContext.isGitRepo(repositoryPath)) { - //TODO: review this for the case of code-settings repo. This could cause issues acc. to claude: - // Combined code-settings repo + --force/--force-pull regression (source-trace-confirmed, niche path — please author-confirm). - // For a combined repo (IDE_HOME/settings → symlink into /settings, .git one level up), - // getRepositoryType(IDE_HOME/settings) classifies as PLAIN_CODE because .git isn't in that folder (RepositoryUtil.java:22-39). - // The non-force case is safe (the guard at :170 skips the pull), but --force/--force-pull bypasses the guard and then checkClonedSettings - // backs up the valid settings and re-clones from scratch instead of pulling; even a passing check would then fail in - // applySettings (which re-derives PLAIN_CODE at :229). On main this path was a plain git pull. No test covers it. + if (isSettingsFolder(repositoryPath) && ideContext.getGitContext().isGitRepo(repositoryPath)) { return RepositoryType.SETTINGS; } Path settingsFolder = repositoryPath.resolve(IdeContext.FOLDER_SETTINGS); if (isSettingsFolder(settingsFolder)) { return RepositoryType.CODE_SETTINGS_COMBINED; } - if (!Files.exists(settingsFolder)) { - return RepositoryType.PLAIN_CODE; - } // there is no valid settings folder to be found. return RepositoryType.UNKNOWN; } + /** + * @param settingsPath + * @param context + * @return + */ + public static RepositoryType ofSettingsPath(Path settingsPath, IdeContext context) { + + if (settingsPath == null || context == null) { + return RepositoryType.UNKNOWN; + } + if (context.getGitContext().isGitRepo(settingsPath)) { + return RepositoryType.SETTINGS; + } else if (isCombinedSettingsCodeRepository(settingsPath, context)) { + return RepositoryType.CODE_SETTINGS_COMBINED; + } + return RepositoryType.UNKNOWN; + } + /** * @return true if repository is either of type {@code SETTINGS} or {@code CODE_SETTINGS_COMBINED} */ - public boolean isSettingsOrCodeSettingsRepository() { + public boolean isValid() { return this == SETTINGS || this == CODE_SETTINGS_COMBINED; } @@ -74,4 +79,20 @@ private static boolean isSettingsFolder(Path folder) { return (Files.exists(folder.resolve(EnvironmentVariables.DEFAULT_PROPERTIES)) || Files.exists(folder.resolve(EnvironmentVariables.LEGACY_PROPERTIES))); } + + private static boolean isCombinedSettingsCodeRepository(Path settingsPath, IdeContext context) { + + FileAccess fileAccess = context.getFileAccess(); + if (settingsPath != null) { + boolean settingsIsLink = Files.isSymbolicLink(settingsPath) || fileAccess.isJunction(settingsPath); + if (settingsIsLink) { + Path realPath = fileAccess.toRealPath(settingsPath); + if (realPath != null) { + return context.getGitContext().isGitRepo(realPath.getParent()); + } + return true; + } + } + return false; + } } From 845ead8e3d007aa01e267a607fc744550013206c Mon Sep 17 00:00:00 2001 From: laim2003 Date: Tue, 8 Sep 2026 17:23:02 +0200 Subject: [PATCH 88/89] #1695: - removed dead code Signed-off-by: laim2003 --- .../update/settings/SettingsUpdater.java | 27 +++---------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index fb956d7c0d..d5d0ecfd19 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -22,8 +22,8 @@ *
  • {@link #checkSettings(Path)} health check: the settings are always cloned into a temporary directory * first where it is verified that the git URL is valid, that cloning succeeded, * and that the repository actually is a settings or a combined code and settings repository.
  • - *
  • {@link #applySettings(boolean, Path)} apply: only after the health check succeeded the settings are either pulled in place (if they were already - * present) or the verified clone is moved to its final location.
  • + *
  • {@link #applySettings(SettingsHealthCheckResult)} apply: Settings are pulled in place (if they were already + * present)
  • * */ public class SettingsUpdater { @@ -126,7 +126,7 @@ private SettingsHealthCheckResult cloneAndCheckSettings(Path settingsPath) { RepositoryType repositoryType = RepositoryType.ofGitRoot(tempCloneDir, this.context); if (!repositoryType.isValid()) { - //see @javadoc why we throw fatally here. + return SettingsHealthCheckResult.ofFailed(repositoryType, MESSAGE_INVALID_REPOSITORY, tempCloneDir, false); } return SettingsHealthCheckResult.ofSuccess(repositoryType, tempCloneDir, false); @@ -195,17 +195,6 @@ Your settings repository seems to be broken ('.git' folder not present). fileAccess.backup(settingsPath); } - /** - * @return {@code true} if the user explicitly wants to continue with an invalid repository, {@code false} otherwise. - */ - private boolean requestUserConfirmInvalidRepository(RepositoryType repositoryType, GitUrl gitUrl) { - - LOG.warn("{}\nURL: {}\nDetected settings repository type: {}", MESSAGE_INVALID_REPOSITORY, gitUrl, repositoryType); - - this.context.askToContinue("The update to the settings repository you are trying to apply seems to be broken. Do you want to continue anyway?"); - return true; - } - /** * Applies the result of the {@link #checkSettings(Path)} health check by either pulling the settings in place or moving the verified clone to its final * location. @@ -224,21 +213,13 @@ public SettingsUpdateResult applySettings(SettingsHealthCheckResult healthCheckR // Case 1: We performed "ide update"; so settings already existed and we just need to perform a git pull in the existing repo. if (onlyPull) { - repositoryType = RepositoryType.ofSettingsPath(context.getSettingsPath(), this.context); - - if (repositoryType != RepositoryType.SETTINGS) { - return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, - repositoryType, - "Expected settings repository for update application, but was of type: " + repositoryType); - } - pullSettingsAndSaveCommitId(settingsPath); repositoryType = RepositoryType.ofSettingsPath(context.getSettingsPath(), this.context); if (repositoryType != RepositoryType.SETTINGS) { return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATE_FAILED, repositoryType, - "The updated settings repository seems to be of an invalid type: " + repositoryType); + "Update done, but the updated settings repository seems to be of an invalid type: " + repositoryType); } return new SettingsUpdateResult(SettingsUpdateStatus.SETTINGS_UPDATED, repositoryType, null); From fc662fdb50815574587bf04f5fed1f38d551d0ac Mon Sep 17 00:00:00 2001 From: laim2003 Date: Wed, 9 Sep 2026 11:23:54 +0200 Subject: [PATCH 89/89] #1695: - removed dead code Signed-off-by: laim2003 --- .../tools/ide/commandlet/update/settings/SettingsUpdater.java | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java index d5d0ecfd19..fda8a8c64f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/update/settings/SettingsUpdater.java @@ -142,7 +142,6 @@ private SettingsHealthCheckResult cloneAndCheckSettings(Path settingsPath) { * {@link CliException#getExitCode() exit code} so that e.g. an abort by the user is still reported as such. */ private static CliFatalException createGuaranteedFatalException(RuntimeException error) { - //TODO: Dont drop the exit code here if (error instanceof CliFatalException rethrow) { return rethrow;