> propertyIterator = cmd.getValues().iterator();
Property> property = null;
if (propertyIterator.hasNext()) {
property = propertyIterator.next();
@@ -960,15 +1023,15 @@ public ValidationResult apply(CliArguments arguments, Commandlet cmd, Completion
arguments.stopSplitShortOptions();
}
}
- boolean matches = currentProperty.apply(arguments, this, cmd, collector);
- if (!matches || currentArgument.isCompletion()) {
+ boolean matches = currentProperty.apply(arguments, this, cmd, null);
+ if (!matches && currentArgument.isCompletion()) {
ValidationState state = new ValidationState(null);
state.addErrorMessage("No matching property found");
return state;
}
currentArgument = arguments.current();
}
- return new ValidationState(null);
+ return ValidationResultValid.get();
}
@Override
diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/GitContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/GitContext.java
deleted file mode 100644
index 99754889c..000000000
--- a/cli/src/main/java/com/devonfw/tools/ide/context/GitContext.java
+++ /dev/null
@@ -1,218 +0,0 @@
-package com.devonfw.tools.ide.context;
-
-import java.nio.file.Path;
-
-import com.devonfw.tools.ide.cli.CliOfflineException;
-
-/**
- * Interface for git commands with input and output of information for the user.
- */
-public interface GitContext {
-
- /** The default git remote name. */
- String DEFAULT_REMOTE = "origin";
-
- /** The default git url of the settings repository for IDEasy developers */
- String DEFAULT_SETTINGS_GIT_URL = "https://github.com/devonfw/ide-settings.git";
-
- /** The name of the internal metadata folder of a git repository. */
- String GIT_FOLDER = ".git";
-
- /**
- * Checks if the Git repository in the specified target folder needs an update by inspecting the modification time of a magic file.
- *
- * @param repoUrl the git remote URL to clone from.
- * @param branch the explicit name of the branch to checkout e.g. "main" or {@code null} to use the default branch.
- * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where
- * git will by default create a sub-folder by default on clone but the final folder that will contain the ".git" subfolder.
- * @throws CliOfflineException if offline and cloning is needed.
- */
- void pullOrCloneIfNeeded(String repoUrl, String branch, Path targetRepository);
-
- /**
- * Checks if a git fetch is needed and performs it if required.
- *
- * This method checks the last modified time of the `FETCH_HEAD` file in the `.git` directory to determine if a fetch is needed based on a predefined
- * threshold. If updates are available in the remote repository, it logs an information message prompting the user to pull the latest changes.
- *
- * @param targetRepository the {@link Path} to the target folder where the git repository is located. It contains the `.git` subfolder.
- * @return {@code true} if updates were detected after fetching from the remote repository, indicating that the local repository is behind the remote. *
- * {@code false} if no updates were detected or if no fetching was performed (e.g., the cache threshold was not met or the context is offline)
- */
- boolean fetchIfNeeded(Path targetRepository);
-
- /**
- * Checks if a git fetch is needed and performs it if required.
- *
- * This method checks the last modified time of the `FETCH_HEAD` file in the `.git` directory to determine if a fetch is needed based on a predefined
- * threshold. If updates are available in the remote repository, it logs an information message prompting the user to pull the latest changes.
- *
- * @param targetRepository the {@link Path} to the target folder where the git repository is located. It contains the `.git` subfolder.
- * @param remoteName the name of the remote repository, e.g., "origin".
- * @param branch the name of the branch to check for updates.
- * @return {@code true} if updates were detected after fetching from the remote repository, indicating that the local repository is behind the remote.
- * {@code false} if no updates were detected or if no fetching was performed (e.g., the cache threshold was not met or the context is offline)
- */
- boolean fetchIfNeeded(Path targetRepository, String remoteName, String branch);
-
- /**
- * Checks if there are updates available for the Git repository in the specified target folder by comparing the local commit hash with the remote commit
- * hash.
- *
- * @param targetRepository the {@link Path} to the target folder where the git repository is located. This should be the folder containing the ".git"
- * subfolder.
- * @return {@code true} if the remote repository contains commits that are not present in the local repository, indicating that updates are available.
- * {@code false} if the local and remote repositories are in sync, or if there was an issue retrieving the commit hashes.
- */
- boolean isRepositoryUpdateAvailable(Path targetRepository);
-
- /**
- * Attempts a git pull and reset if required.
- *
- * @param repoUrl the git remote URL to clone from.
- * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where
- * git will by default create a sub-folder by default on clone but the final folder that will contain the ".git" subfolder.
- * @throws CliOfflineException if offline and cloning is needed.
- */
- default void pullOrCloneAndResetIfNeeded(String repoUrl, Path targetRepository) {
-
- pullOrCloneAndResetIfNeeded(repoUrl, targetRepository, null);
- }
-
- /**
- * Attempts a git pull and reset if required.
- *
- * @param repoUrl the git remote URL to clone from.
- * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where
- * git will by default create a sub-folder by default on clone but the final folder that will contain the ".git" subfolder.
- * @param branch the explicit name of the branch to checkout e.g. "main" or {@code null} to use the default branch.
- * @throws CliOfflineException if offline and cloning is needed.
- */
- default void pullOrCloneAndResetIfNeeded(String repoUrl, Path targetRepository, String branch) {
-
- pullOrCloneAndResetIfNeeded(repoUrl, targetRepository, branch, null);
- }
-
- /**
- * Attempts a git pull and reset if required.
- *
- * @param repoUrl the git remote URL to clone from.
- * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where
- * git will by default create a sub-folder by default on clone but the final folder that will contain the ".git" subfolder.
- * @param branch the explicit name of the branch to checkout e.g. "main" or {@code null} to use the default branch.
- * @param remoteName the remote name e.g. origin.
- * @throws CliOfflineException if offline and cloning is needed.
- */
- void pullOrCloneAndResetIfNeeded(String repoUrl, Path targetRepository, String branch, String remoteName);
-
- /**
- * Runs a git pull or a git clone.
- *
- * @param gitRepoUrl the git remote URL to clone from.
- * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where
- * git will by default create a sub-folder by default on clone but the final folder that will contain the ".git" subfolder.
- * @throws CliOfflineException if offline and cloning is needed.
- */
- void pullOrClone(String gitRepoUrl, Path targetRepository);
-
- /**
- * Runs a git pull or a git clone.
- *
- * @param gitRepoUrl the git remote URL to clone from.
- * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where
- * git will by default create a sub-folder by default on clone but the final folder that will contain the ".git" subfolder.
- * @param branch the explicit name of the branch to checkout e.g. "main" or {@code null} to use the default branch.
- * @throws CliOfflineException if offline and cloning is needed.
- */
- void pullOrClone(String gitRepoUrl, Path targetRepository, String branch);
-
- /**
- * Runs a git clone.
- *
- * @param gitRepoUrl the {@link GitUrl} to use for the repository URL.
- * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where
- * git will by default create a sub-folder by default on clone but the * final folder that will contain the ".git" subfolder.
- * @throws CliOfflineException if offline and cloning is needed.
- */
- void clone(GitUrl gitRepoUrl, Path targetRepository);
-
- /**
- * Runs a git pull.
- *
- * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where
- * git will by default create a sub-folder by default on clone but the * final folder that will contain the ".git" subfolder.
- */
- void pull(Path targetRepository);
-
- /**
- * Runs a git diff-index to detect local changes and if so reverts them via git reset.
- *
- * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where
- * git will by default create a sub-folder by default on clone but the final folder that will contain the ".git" subfolder.
- */
- default void reset(Path targetRepository) {
-
- reset(targetRepository, null);
- }
-
- /**
- * Runs a git diff-index to detect local changes and if so reverts them via git reset.
- *
- * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where
- * git will by default create a sub-folder by default on clone but the final folder that will contain the ".git" subfolder.
- * @param branch the explicit name of the branch to checkout e.g. "main" or {@code null} to use the default branch.
- */
- default void reset(Path targetRepository, String branch) {
-
- reset(targetRepository, branch, null);
- }
-
- /**
- * Runs a git fetch.
- *
- * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where
- * git will by default create a sub-folder by default on clone but the final folder that will contain the ".git" subfolder.
- * @param remote the name of the remote repository, e.g., "origin". If {@code null} or empty, the default remote name "origin" will be used.
- * @param branch the name of the branch to check for updates.
- */
- void fetch(Path targetRepository, String remote, String branch);
-
- /**
- * Runs a git reset reverting all local changes to the git repository.
- *
- * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where
- * git will by default create a sub-folder by default on clone but the * final folder that will contain the ".git" subfolder.
- * @param branch the explicit name of the branch to checkout e.g. "main" or {@code null} to use the default branch.
- * @param remoteName the name of the git remote e.g. "origin".
- */
- void reset(Path targetRepository, String branch, String remoteName);
-
- /**
- * Runs a git cleanup if untracked files were found.
- *
- * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where
- * git will by default create a sub-folder by default on clone but the * final folder that will contain the ".git" subfolder.
- */
- void cleanup(Path targetRepository);
-
- /**
- * Returns the URL of a git repository
- *
- * @param repository the {@link Path} to the folder where the git repository is located.
- * @return the url of the repository as a {@link String}.
- */
- String retrieveGitUrl(Path repository);
-
- /**
- * @param repository the {@link Path} to the folder where the git repository is located.
- * @return the name of the current branch.
- */
- String determineCurrentBranch(Path repository);
-
- /**
- * @param repository the {@link Path} to the folder where the git repository is located.
- * @return the name of the default origin.
- */
- String determineRemote(Path repository);
-
-}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/GitContextImpl.java b/cli/src/main/java/com/devonfw/tools/ide/context/GitContextImpl.java
deleted file mode 100644
index 23613c117..000000000
--- a/cli/src/main/java/com/devonfw/tools/ide/context/GitContextImpl.java
+++ /dev/null
@@ -1,297 +0,0 @@
-package com.devonfw.tools.ide.context;
-
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.List;
-import java.util.Objects;
-
-import com.devonfw.tools.ide.cli.CliOfflineException;
-import com.devonfw.tools.ide.process.ProcessContext;
-import com.devonfw.tools.ide.process.ProcessErrorHandling;
-import com.devonfw.tools.ide.process.ProcessMode;
-import com.devonfw.tools.ide.process.ProcessResult;
-import com.devonfw.tools.ide.variable.IdeVariables;
-
-/**
- * Implements the {@link GitContext}.
- */
-public class GitContextImpl implements GitContext {
-
- private final IdeContext context;
-
- private final ProcessContext processContext;
-
- private static final ProcessMode PROCESS_MODE = ProcessMode.DEFAULT;
- private static final ProcessMode PROCESS_MODE_FOR_FETCH = ProcessMode.DEFAULT_CAPTURE;
-
- /**
- * @param context the {@link IdeContext context}.
- */
- public GitContextImpl(IdeContext context) {
-
- this.context = context;
- this.processContext = this.context.newProcess().executable("git").withEnvVar("GIT_TERMINAL_PROMPT", "0").errorHandling(ProcessErrorHandling.LOG_WARNING);
- }
-
- @Override
- public void pullOrCloneIfNeeded(String repoUrl, String branch, Path targetRepository) {
-
- GitOperation.PULL_OR_CLONE.executeIfNeeded(this.context, repoUrl, targetRepository, null, branch);
- }
-
- @Override
- public boolean fetchIfNeeded(Path targetRepository) {
-
- return fetchIfNeeded(targetRepository, null, null);
- }
-
- @Override
- public boolean fetchIfNeeded(Path targetRepository, String remote, String branch) {
-
- return GitOperation.FETCH.executeIfNeeded(this.context, null, targetRepository, remote, branch);
- }
-
- @Override
- public boolean isRepositoryUpdateAvailable(Path repository) {
-
- ProcessResult result = this.processContext.directory(repository).addArg("rev-parse").addArg("HEAD").run(PROCESS_MODE_FOR_FETCH);
- if (!result.isSuccessful()) {
- this.context.warning("Failed to get the local commit hash.");
- return false;
- }
- String localCommitHash = result.getOut().stream().findFirst().orElse("").trim();
- // get remote commit code
- result = this.processContext.addArg("rev-parse").addArg("@{u}").run(PROCESS_MODE_FOR_FETCH);
- if (!result.isSuccessful()) {
- this.context.warning("Failed to get the remote commit hash.");
- return false;
- }
- String remote_commit_code = result.getOut().stream().findFirst().orElse("").trim();
- return !localCommitHash.equals(remote_commit_code);
- }
-
- @Override
- public void pullOrCloneAndResetIfNeeded(String repoUrl, Path repository, String branch, String remoteName) {
-
- pullOrCloneIfNeeded(repoUrl, branch, repository);
-
- reset(repository, "master", remoteName);
-
- cleanup(repository);
- }
-
- @Override
- public void pullOrClone(String gitRepoUrl, Path repository) {
-
- pullOrClone(gitRepoUrl, repository, null);
- }
-
- @Override
- public void pullOrClone(String gitRepoUrl, Path repository, String branch) {
-
- Objects.requireNonNull(repository);
- Objects.requireNonNull(gitRepoUrl);
- if (!gitRepoUrl.startsWith("http")) {
- throw new IllegalArgumentException("Invalid git URL '" + gitRepoUrl + "'!");
- }
- if (Files.isDirectory(repository.resolve(GIT_FOLDER))) {
- // checks for remotes
- String remote = determineRemote(repository);
- if (remote == null) {
- String message = repository + " is a local git repository with no remote - if you did this for testing, you may continue...\n"
- + "Do you want to ignore the problem and continue anyhow?";
- this.context.askToContinue(message);
- } else {
- pull(repository);
- }
- } else {
- clone(new GitUrl(gitRepoUrl, branch), repository);
- }
- }
-
- /**
- * Handles errors which occurred during git pull.
- *
- * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where
- * git will by default create a sub-folder by default on clone but the * final folder that will contain the ".git" subfolder.
- * @param result the {@link ProcessResult} to evaluate.
- */
- private void handleErrors(Path targetRepository, ProcessResult result) {
-
- if (!result.isSuccessful()) {
- String message = "Failed to update git repository at " + targetRepository;
- if (this.context.isOffline()) {
- this.context.warning(message);
- this.context.interaction("Continuing as we are in offline mode - results may be outdated!");
- } else {
- this.context.error(message);
- if (this.context.isOnline()) {
- this.context.error("See above error for details. If you have local changes, please stash or revert and retry.");
- } else {
- this.context.error("It seems you are offline - please ensure Internet connectivity and retry or activate offline mode (-o or --offline).");
- }
- this.context.askToContinue("Typically you should abort and fix the problem. Do you want to continue anyways?");
- }
- }
- }
-
- @Override
- public void clone(GitUrl gitRepoUrl, Path targetRepository) {
-
- GitUrlSyntax gitUrlSyntax = IdeVariables.PREFERRED_GIT_PROTOCOL.get(getContext());
- gitRepoUrl = gitUrlSyntax.format(gitRepoUrl);
- this.processContext.directory(targetRepository);
- ProcessResult result;
- if (!this.context.isOffline()) {
- this.context.getFileAccess().mkdirs(targetRepository);
- this.context.requireOnline("git clone of " + gitRepoUrl.url());
- this.processContext.addArg("clone");
- if (this.context.isQuietMode()) {
- this.processContext.addArg("-q");
- }
- this.processContext.addArgs("--recursive", gitRepoUrl.url(), "--config", "core.autocrlf=false", ".");
- result = this.processContext.run(PROCESS_MODE);
- if (!result.isSuccessful()) {
- this.context.warning("Git failed to clone {} into {}.", gitRepoUrl.url(), targetRepository);
- }
- String branch = gitRepoUrl.branch();
- if (branch != null) {
- this.processContext.addArgs("checkout", branch);
- result = this.processContext.run(PROCESS_MODE);
- if (!result.isSuccessful()) {
- this.context.warning("Git failed to checkout to branch {}", branch);
- }
- }
- } else {
- throw CliOfflineException.ofClone(gitRepoUrl.parseUrl(), targetRepository);
- }
- }
-
- @Override
- public void pull(Path repository) {
-
- if (this.context.isOffline()) {
- this.context.info("Skipping git pull on {} because offline", repository);
- return;
- }
- ProcessResult result = this.processContext.directory(repository).addArg("--no-pager").addArg("pull").addArg("--quiet").run(PROCESS_MODE);
- if (!result.isSuccessful()) {
- String branchName = determineCurrentBranch(repository);
- this.context.warning("Git pull on branch {} failed for repository {}.", branchName, repository);
- handleErrors(repository, result);
- }
- }
-
- @Override
- public void fetch(Path targetRepository, String remote, String branch) {
-
- if (branch == null) {
- branch = determineCurrentBranch(targetRepository);
- }
- if (remote == null) {
- remote = determineRemote(targetRepository);
- }
- ProcessResult result = this.processContext.directory(targetRepository).addArg("fetch").addArg(remote).addArg(branch).run(PROCESS_MODE_FOR_FETCH);
-
- if (!result.isSuccessful()) {
- this.context.warning("Git fetch for '{}/{} failed.'.", remote, branch);
- }
- }
-
- @Override
- public String determineCurrentBranch(Path repository) {
-
- ProcessResult remoteResult = this.processContext.directory(repository).addArg("branch").addArg("--show-current").run(ProcessMode.DEFAULT_CAPTURE);
- if (remoteResult.isSuccessful()) {
- List remotes = remoteResult.getOut();
- if (!remotes.isEmpty()) {
- assert (remotes.size() == 1);
- return remotes.get(0);
- }
- } else {
- this.context.warning("Failed to determine current branch of git repository {}", repository);
- }
- return null;
- }
-
- @Override
- public String determineRemote(Path repository) {
-
- ProcessResult remoteResult = this.processContext.directory(repository).addArg("remote").run(ProcessMode.DEFAULT_CAPTURE);
- if (remoteResult.isSuccessful()) {
- List remotes = remoteResult.getOut();
- if (!remotes.isEmpty()) {
- assert (remotes.size() == 1);
- return remotes.get(0);
- }
- } else {
- this.context.warning("Failed to determine current origin of git repository {}", repository);
- }
- return null;
- }
-
- @Override
- public void reset(Path targetRepository, String branchName, String remoteName) {
-
- if ((remoteName == null) || remoteName.isEmpty()) {
- remoteName = DEFAULT_REMOTE;
- }
- this.processContext.directory(targetRepository);
- ProcessResult result;
- // check for changed files
- result = this.processContext.addArg("diff-index").addArg("--quiet").addArg("HEAD").run(PROCESS_MODE);
-
- if (!result.isSuccessful()) {
- // reset to origin/master
- this.context.warning("Git has detected modified files -- attempting to reset {} to '{}/{}'.", targetRepository, remoteName, branchName);
- result = this.processContext.addArg("reset").addArg("--hard").addArg(remoteName + "/" + branchName).run(PROCESS_MODE);
-
- if (!result.isSuccessful()) {
- this.context.warning("Git failed to reset {} to '{}/{}'.", remoteName, branchName, targetRepository);
- handleErrors(targetRepository, result);
- }
- }
- }
-
- @Override
- public void cleanup(Path targetRepository) {
-
- this.processContext.directory(targetRepository);
- ProcessResult result;
- // check for untracked files
- result = this.processContext.addArg("ls-files").addArg("--other").addArg("--directory").addArg("--exclude-standard").run(ProcessMode.DEFAULT_CAPTURE);
-
- if (!result.getOut().isEmpty()) {
- // delete untracked files
- this.context.warning("Git detected untracked files in {} and is attempting a cleanup.", targetRepository);
- result = this.processContext.addArg("clean").addArg("-df").run(PROCESS_MODE);
-
- if (!result.isSuccessful()) {
- this.context.warning("Git failed to clean the repository {}.", targetRepository);
- }
- }
- }
-
- @Override
- public String retrieveGitUrl(Path repository) {
-
- this.processContext.directory(repository);
- ProcessResult result;
- result = this.processContext.addArgs("-C", repository, "remote", "-v").run(ProcessMode.DEFAULT_CAPTURE);
- for (String line : result.getOut()) {
- if (line.contains("(fetch)")) {
- return line.split("\\s+")[1]; // Extract the URL from the line
- }
- }
-
- this.context.error("Failed to retrieve git URL for repository: {}", repository);
- return null;
- }
-
- IdeContext getContext() {
-
- return this.context;
- }
-}
-
-
diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/GitUrl.java b/cli/src/main/java/com/devonfw/tools/ide/context/GitUrl.java
deleted file mode 100644
index 510c07024..000000000
--- a/cli/src/main/java/com/devonfw/tools/ide/context/GitUrl.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.devonfw.tools.ide.context;
-
-import java.net.MalformedURLException;
-import java.net.URL;
-
-/**
- * Handles parsing of git URLs.
- *
- * @param url the git url e.g. https://github.com/devonfw/ide-urls.git.
- * @param branch the branch name e.g. master.
- */
-public record GitUrl(String url, String branch) {
-
- /**
- * Converts the Git URL based on the specified {@link GitUrlSyntax}.
- *
- * @param syntax the preferred {@link GitUrlSyntax} (SSH or HTTPS).
- * @return the converted {@link GitUrl} or the original if no conversion is required.
- */
- public GitUrl convert(GitUrlSyntax syntax) {
- return syntax.format(this);
- }
-
- /**
- * Parses a git URL and omits the branch name if not provided.
- *
- * @return parsed URL.
- */
- public URL parseUrl() {
-
- String parsedUrl = this.url;
- if (this.branch != null && !this.branch.isEmpty()) {
- parsedUrl += "#" + this.branch;
- }
- URL validUrl;
- try {
- validUrl = new URL(parsedUrl);
- } catch (MalformedURLException e) {
- throw new RuntimeException("Git URL is not valid " + parsedUrl, e);
- }
- return validUrl;
- }
-}
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 077398198..0273629a5 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
@@ -10,10 +10,11 @@
import com.devonfw.tools.ide.common.SystemPath;
import com.devonfw.tools.ide.environment.EnvironmentVariables;
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
+import com.devonfw.tools.ide.environment.IdeSystem;
+import com.devonfw.tools.ide.git.GitContext;
import com.devonfw.tools.ide.io.FileAccess;
import com.devonfw.tools.ide.io.IdeProgressBar;
import com.devonfw.tools.ide.merge.DirectoryMerger;
-import com.devonfw.tools.ide.network.ProxyContext;
import com.devonfw.tools.ide.os.SystemInfo;
import com.devonfw.tools.ide.os.WindowsPathSyntax;
import com.devonfw.tools.ide.process.ProcessContext;
@@ -398,13 +399,50 @@ default Path getSettingsTemplatePath() {
ProcessContext newProcess();
/**
- * Prepares the {@link IdeProgressBar} initializes task name and maximum size as well as the behaviour and style.
- *
- * @param taskName name of the task.
- * @param size of the content.
- * @return {@link IdeProgressBar} to use.
+ * @param title the {@link IdeProgressBar#getTitle() title}.
+ * @param size the {@link IdeProgressBar#getMaxSize() expected maximum size}.
+ * @param unitName the {@link IdeProgressBar#getUnitName() unit name}.
+ * @param unitSize the {@link IdeProgressBar#getUnitSize() unit size}.
+ * @return the new {@link IdeProgressBar} to use.
+ */
+ IdeProgressBar newProgressBar(String title, long size, String unitName, long unitSize);
+
+ /**
+ * @param title the {@link IdeProgressBar#getTitle() title}.
+ * @param size the {@link IdeProgressBar#getMaxSize() expected maximum size} in bytes.
+ * @return the new {@link IdeProgressBar} to use.
+ */
+ default IdeProgressBar newProgressBarInMib(String title, long size) {
+
+ return newProgressBar(title, size, "MiB", 1048576);
+ }
+
+ /**
+ * @param size the {@link IdeProgressBar#getMaxSize() expected maximum size} in bytes.
+ * @return the new {@link IdeProgressBar} for copy.
+ */
+ default IdeProgressBar newProgressBarForDownload(long size) {
+
+ return newProgressBarInMib(IdeProgressBar.TITLE_DOWNLOADING, size);
+ }
+
+ /**
+ * @param size the {@link IdeProgressBar#getMaxSize() expected maximum size} in bytes.
+ * @return the new {@link IdeProgressBar} for extracting.
*/
- IdeProgressBar prepareProgressBar(String taskName, long size);
+ default IdeProgressBar newProgressbarForExtracting(long size) {
+
+ return newProgressBarInMib(IdeProgressBar.TITLE_EXTRACTING, size);
+ }
+
+ /**
+ * @param size the {@link IdeProgressBar#getMaxSize() expected maximum size} in bytes.
+ * @return the new {@link IdeProgressBar} for copy.
+ */
+ default IdeProgressBar newProgressbarForCopying(long size) {
+
+ return newProgressBarInMib(IdeProgressBar.TITLE_COPYING, size);
+ }
/**
* @return the {@link DirectoryMerger} used to configure and merge the workspace for an {@link com.devonfw.tools.ide.tool.ide.IdeToolCommandlet IDE}.
@@ -416,7 +454,10 @@ default Path getSettingsTemplatePath() {
*/
Path getDefaultExecutionDirectory();
- ProxyContext getProxyContext();
+ /**
+ * @return the {@link IdeSystem} instance wrapping {@link System}.
+ */
+ IdeSystem getSystem();
/**
* @return the {@link GitContext} used to run several git commands.
@@ -542,4 +583,9 @@ default String findBashRequired() {
*/
WindowsPathSyntax getPathSyntax();
+ /**
+ * logs the status of {@link #getIdeHome() IDE_HOME} and {@link #getIdeRoot() IDE_ROOT}.
+ */
+ void logIdeHomeAndRootStatus();
+
}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContextConsole.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContextConsole.java
index 7f242d222..d643df1c4 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContextConsole.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContextConsole.java
@@ -23,7 +23,7 @@ public class IdeContextConsole extends AbstractIdeContext {
*/
public IdeContextConsole(IdeLogLevel minLogLevel, Appendable out, boolean colored) {
- super(new IdeStartContextImpl(minLogLevel, level -> new IdeSubLoggerOut(level, out, colored, minLogLevel)), null);
+ super(new IdeStartContextImpl(minLogLevel, level -> new IdeSubLoggerOut(level, out, colored, minLogLevel, null)), null);
if (System.console() == null) {
debug("System console not available - using System.in as fallback");
this.scanner = new Scanner(System.in);
@@ -59,7 +59,8 @@ protected String readLine() {
}
@Override
- public IdeProgressBar prepareProgressBar(String taskName, long size) {
- return new IdeProgressBarConsole(getSystemInfo(), taskName, size);
+ public IdeProgressBar newProgressBar(String title, long size, String unitName, long unitSize) {
+
+ return new IdeProgressBarConsole(getSystemInfo(), title, size, unitName, unitSize);
}
}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/IdeStartContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeStartContext.java
index c95bf94df..e1a9b13eb 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeStartContext.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeStartContext.java
@@ -31,6 +31,11 @@ public interface IdeStartContext extends IdeLogger {
*/
boolean isOfflineMode();
+ /**
+ * @return {@code true} if quickStart mode is activated (-s/--quickStart), {@code false} otherwise.
+ */
+ boolean isSkipUpdatesMode();
+
/**
* @return the current {@link Locale}. Either configured via command-line option or {@link Locale#getDefault() default}.
*/
diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/IdeStartContextImpl.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeStartContextImpl.java
index 8d826dd02..b7963c53c 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeStartContextImpl.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeStartContextImpl.java
@@ -1,21 +1,19 @@
package com.devonfw.tools.ide.context;
import java.util.Locale;
-import java.util.Objects;
import java.util.function.Function;
+import com.devonfw.tools.ide.log.AbstractIdeSubLogger;
import com.devonfw.tools.ide.log.IdeLogLevel;
+import com.devonfw.tools.ide.log.IdeLoggerImpl;
import com.devonfw.tools.ide.log.IdeSubLogger;
-import com.devonfw.tools.ide.log.IdeSubLoggerNone;
/**
* Implementation of {@link IdeStartContext}.
*/
-public class IdeStartContextImpl implements IdeStartContext {
+public class IdeStartContextImpl extends IdeLoggerImpl implements IdeStartContext {
- private final Function loggerFactory;
-
- private final IdeSubLogger[] loggers;
+ private boolean skipUpdatesMode;
private boolean offlineMode;
@@ -31,48 +29,9 @@ public class IdeStartContextImpl implements IdeStartContext {
* @param minLogLevel the minimum enabled {@link IdeLogLevel}.
* @param factory the factory to create active {@link IdeSubLogger} instances.
*/
- public IdeStartContextImpl(IdeLogLevel minLogLevel, Function factory) {
+ public IdeStartContextImpl(IdeLogLevel minLogLevel, Function factory) {
- super();
- this.loggerFactory = factory;
- this.loggers = new IdeSubLogger[IdeLogLevel.values().length];
- setLogLevel(minLogLevel);
- }
-
- @Override
- public IdeSubLogger level(IdeLogLevel level) {
-
- IdeSubLogger logger = this.loggers[level.ordinal()];
- Objects.requireNonNull(logger);
- return logger;
- }
-
- /**
- * Sets the log level.
- *
- * @param logLevel {@link IdeLogLevel}
- */
- public void setLogLevel(IdeLogLevel logLevel) {
-
- for (IdeLogLevel level : IdeLogLevel.values()) {
- boolean enabled = level.ordinal() >= logLevel.ordinal();
- setLogLevel(level, enabled);
- }
- }
-
- /**
- * @param logLevel the {@link IdeLogLevel} to modify.
- * @param enabled - {@code true} to enable, {@code false} to disable.
- */
- public void setLogLevel(IdeLogLevel logLevel, boolean enabled) {
-
- IdeSubLogger logger;
- if (enabled) {
- logger = this.loggerFactory.apply(logLevel);
- } else {
- logger = IdeSubLoggerNone.of(logLevel);
- }
- this.loggers[logLevel.ordinal()] = logger;
+ super(minLogLevel, factory);
}
@Override
@@ -131,6 +90,20 @@ public void setOfflineMode(boolean offlineMode) {
this.offlineMode = offlineMode;
}
+ @Override
+ public boolean isSkipUpdatesMode() {
+
+ return this.skipUpdatesMode;
+ }
+
+ /**
+ * @param skipUpdatesMode new value of {@link #isSkipUpdatesMode()} ()}.
+ */
+ public void setSkipUpdatesMode(boolean skipUpdatesMode) {
+
+ this.skipUpdatesMode = skipUpdatesMode;
+ }
+
@Override
public Locale getLocale() {
diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java b/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java
index 8983a0166..52445bf7b 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java
@@ -73,6 +73,12 @@ public Path getPropertiesFilePath() {
return null;
}
+ @Override
+ public Path getLegacyPropertiesFilePath() {
+
+ return null;
+ }
+
@Override
public VariableSource getSource() {
diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariables.java b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariables.java
index 40339213c..de52a53e8 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariables.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariables.java
@@ -118,6 +118,11 @@ default EnvironmentVariables getByType(EnvironmentVariablesType type) {
*/
Path getPropertiesFilePath();
+ /**
+ * @return the {@link Path} to the {@link #LEGACY_PROPERTIES} if they exist for this {@link EnvironmentVariables} or {@code null} otherwise (does not exist).
+ */
+ Path getLegacyPropertiesFilePath();
+
/**
* @return the {@link VariableSource} of this {@link EnvironmentVariables}.
*/
diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFile.java b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFile.java
index 64e1d9cd6..877b43295 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFile.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFile.java
@@ -252,6 +252,22 @@ public Path getPropertiesFilePath() {
return this.propertiesFilePath;
}
+ @Override
+ public Path getLegacyPropertiesFilePath() {
+
+ if (this.propertiesFilePath == null) {
+ return null;
+ }
+ if (this.propertiesFilePath.getFileName().toString().equals(LEGACY_PROPERTIES)) {
+ return this.propertiesFilePath;
+ }
+ Path legacyProperties = this.propertiesFilePath.getParent().resolve(LEGACY_PROPERTIES);
+ if (Files.exists(legacyProperties)) {
+ return legacyProperties;
+ }
+ return null;
+ }
+
@Override
public String set(String name, String value, boolean export) {
diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesSystem.java b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesSystem.java
index d2bde2173..0ec6d8b0f 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesSystem.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesSystem.java
@@ -25,7 +25,7 @@ public EnvironmentVariablesType getType() {
@Override
protected Map getVariables() {
- return System.getenv();
+ return this.context.getSystem().getEnv();
}
/**
diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/IdeSystem.java b/cli/src/main/java/com/devonfw/tools/ide/environment/IdeSystem.java
new file mode 100644
index 000000000..ba03bcddf
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/environment/IdeSystem.java
@@ -0,0 +1,44 @@
+package com.devonfw.tools.ide.environment;
+
+import java.util.Map;
+
+/**
+ * Interface to abstract from {@link System}.
+ */
+public interface IdeSystem {
+
+ /**
+ * @param key the name of the requested system property.
+ * @return the {@link System#getProperty(String) value} of the requested system property.
+ * @see System#getProperty(String)
+ */
+ String getProperty(String key);
+
+ /**
+ * @param key the name of the requested system property.
+ * @param fallback the value to return as default in case the requested system property is undefined.
+ * @return the {@link System#getProperty(String, String) value} of the requested system property.
+ * @see System#getProperty(String, String)
+ */
+ String getProperty(String key, String fallback);
+
+ /**
+ * @param key the name of the system property to set.
+ * @param value the new value to {@link System#setProperty(String, String) set}.
+ * @see System#setProperty(String, String)
+ */
+ void setProperty(String key, String value);
+
+ /**
+ * @param key the name of the requested environment variable.
+ * @return the {@link System#getenv(String) value} of the requested environment variable.
+ * @see System#getenv(String)
+ */
+ String getEnv(String key);
+
+ /**
+ * @return the {@link System#getenv() environment variables}.
+ */
+ Map getEnv();
+
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/IdeSystemImpl.java b/cli/src/main/java/com/devonfw/tools/ide/environment/IdeSystemImpl.java
new file mode 100644
index 000000000..880e11222
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/environment/IdeSystemImpl.java
@@ -0,0 +1,80 @@
+package com.devonfw.tools.ide.environment;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+
+import com.devonfw.tools.ide.log.IdeLogger;
+
+/**
+ * Implementation of {@link IdeSystem}.
+ */
+public class IdeSystemImpl implements IdeSystem {
+
+ private final IdeLogger logger;
+
+ final Properties systemProperties;
+
+ final Map environmentVariables;
+
+ /**
+ * @param logger the {@link IdeLogger}.
+ */
+ public IdeSystemImpl(IdeLogger logger) {
+
+ this(logger, System.getProperties(), System.getenv());
+ }
+
+ /**
+ * @param logger the {@link IdeLogger}.
+ * @param systemProperties the {@link System#getProperties() system properties}.
+ * @param environmentVariables the {@link System#getenv() environment variables}.
+ */
+ protected IdeSystemImpl(IdeLogger logger, Properties systemProperties, Map environmentVariables) {
+
+ super();
+ this.logger = logger;
+ this.systemProperties = systemProperties;
+ this.environmentVariables = environmentVariables;
+ }
+
+ @Override
+ public String getProperty(String key) {
+
+ return this.systemProperties.getProperty(key);
+ }
+
+ @Override
+ public String getProperty(String key, String fallback) {
+
+ return this.systemProperties.getProperty(key, fallback);
+ }
+
+ @Override
+ public void setProperty(String key, String value) {
+
+ String old = getProperty(key);
+ if (Objects.equals(old, value)) {
+ this.logger.trace("System property was already set to {}={}", key, value);
+ } else {
+ this.systemProperties.put(key, value);
+ if (old == null) {
+ this.logger.trace("System property was set to {}={}", key, value);
+ } else {
+ this.logger.trace("System property was changed to {}={} from {}", key, value, old);
+ }
+ }
+ }
+
+ @Override
+ public String getEnv(String key) {
+
+ return this.environmentVariables.get(key);
+ }
+
+ @Override
+ public Map getEnv() {
+
+ return this.environmentVariables;
+ }
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java
new file mode 100644
index 000000000..fe5af87e9
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java
@@ -0,0 +1,178 @@
+package com.devonfw.tools.ide.git;
+
+import java.nio.file.Path;
+
+import com.devonfw.tools.ide.cli.CliOfflineException;
+
+/**
+ * Interface for git commands with input and output of information for the user.
+ */
+public interface GitContext {
+
+ /** The default git remote name. */
+ String DEFAULT_REMOTE = "origin";
+
+ /** The default git url of the settings repository for IDEasy developers */
+ String DEFAULT_SETTINGS_GIT_URL = "https://github.com/devonfw/ide-settings.git";
+
+ /** The name of the internal metadata folder of a git repository. */
+ String GIT_FOLDER = ".git";
+
+ /**
+ * Checks if the Git repository in the specified target folder needs an update by inspecting the modification time of a magic file.
+ *
+ * @param gitUrl the {@link GitUrl} to clone from.
+ * @param repository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where git
+ * will by default create a sub-folder by default on clone but the final folder that will contain the ".git" subfolder.
+ * @throws CliOfflineException if offline and cloning is needed.
+ */
+ void pullOrCloneIfNeeded(GitUrl gitUrl, Path repository);
+
+ /**
+ * Checks if a git fetch is needed and performs it if required.
+ *
+ * This method checks the last modified time of the `FETCH_HEAD` file in the `.git` directory to determine if a fetch is needed based on a predefined
+ * threshold. If updates are available in the remote repository, it logs an information message prompting the user to pull the latest changes.
+ *
+ * @param repository the {@link Path} to the target folder where the git repository is located. It contains the `.git` subfolder.
+ * @return {@code true} if updates were detected after fetching from the remote repository, indicating that the local repository is behind the remote. *
+ * {@code false} if no updates were detected or if no fetching was performed (e.g., the cache threshold was not met or the context is offline)
+ */
+ boolean fetchIfNeeded(Path repository);
+
+ /**
+ * Checks if a git fetch is needed and performs it if required.
+ *
+ * This method checks the last modified time of the `FETCH_HEAD` file in the `.git` directory to determine if a fetch is needed based on a predefined
+ * threshold. If updates are available in the remote repository, it logs an information message prompting the user to pull the latest changes.
+ *
+ * @param repository the {@link Path} to the target folder where the git repository is located. It contains the `.git` subfolder.
+ * @param remoteName the name of the remote repository, e.g., "origin".
+ * @param branch the name of the branch to check for updates.
+ * @return {@code true} if updates were detected after fetching from the remote repository, indicating that the local repository is behind the remote.
+ * {@code false} if no updates were detected or if no fetching was performed (e.g., the cache threshold was not met or the context is offline)
+ */
+ boolean fetchIfNeeded(Path repository, String remoteName, String branch);
+
+ /**
+ * Checks if there are updates available for the Git repository in the specified target folder by comparing the local commit hash with the remote commit
+ * hash.
+ *
+ * @param repository the {@link Path} to the target folder where the git repository is located. This should be the folder containing the ".git"
+ * subfolder.
+ * @return {@code true} if the remote repository contains commits that are not present in the local repository, indicating that updates are available.
+ * {@code false} if the local and remote repositories are in sync, or if there was an issue retrieving the commit hashes.
+ */
+ boolean isRepositoryUpdateAvailable(Path repository);
+
+ /**
+ * Attempts a git pull and reset if required.
+ *
+ * @param gitUrl the {@link GitUrl} to clone from.
+ * @param repository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where git
+ * will by default create a sub-folder by default on clone but the final folder that will contain the ".git" subfolder.
+ * @param remoteName the remote name e.g. origin.
+ * @throws CliOfflineException if offline and cloning is needed.
+ */
+ void pullOrCloneAndResetIfNeeded(GitUrl gitUrl, Path repository, String remoteName);
+
+ /**
+ * Runs a git pull or a git clone.
+ *
+ * @param gitUrl the {@link GitUrl} to clone from.
+ * @param repository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where git
+ * will by default create a sub-folder by default on clone but the final folder that will contain the ".git" subfolder.
+ * @throws CliOfflineException if offline and cloning is needed.
+ */
+ void pullOrClone(GitUrl gitUrl, Path repository);
+
+ /**
+ * Runs a git clone.
+ *
+ * @param gitUrl the {@link GitUrl} to use for the repository URL.
+ * @param repository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where git
+ * will by default create a sub-folder by default on clone but the * final folder that will contain the ".git" subfolder.
+ * @throws CliOfflineException if offline and cloning is needed.
+ */
+ void clone(GitUrl gitUrl, Path repository);
+
+ /**
+ * Runs a git pull.
+ *
+ * @param repository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where git
+ * will by default create a sub-folder by default on clone but the * final folder that will contain the ".git" subfolder.
+ */
+ void pull(Path repository);
+
+ /**
+ * Runs a git diff-index to detect local changes and if so reverts them via git reset.
+ *
+ * @param repository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where git
+ * will by default create a sub-folder by default on clone but the final folder that will contain the ".git" subfolder.
+ */
+ default void reset(Path repository) {
+
+ reset(repository, null);
+ }
+
+ /**
+ * Runs a git diff-index to detect local changes and if so reverts them via git reset.
+ *
+ * @param repository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where git
+ * will by default create a sub-folder by default on clone but the final folder that will contain the ".git" subfolder.
+ * @param branch the explicit name of the branch to checkout e.g. "main" or {@code null} to use the default branch.
+ */
+ default void reset(Path repository, String branch) {
+
+ reset(repository, branch, null);
+ }
+
+ /**
+ * Runs a git fetch.
+ *
+ * @param repository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where git
+ * will by default create a sub-folder by default on clone but the final folder that will contain the ".git" subfolder.
+ * @param remote the name of the remote repository, e.g., "origin". If {@code null} or empty, the default remote name "origin" will be used.
+ * @param branch the name of the branch to check for updates.
+ */
+ void fetch(Path repository, String remote, String branch);
+
+ /**
+ * Runs a git reset reverting all local changes to the git repository.
+ *
+ * @param repository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where git
+ * will by default create a sub-folder by default on clone but the * final folder that will contain the ".git" subfolder.
+ * @param branch the explicit name of the branch to checkout e.g. "main" or {@code null} to use the default branch.
+ * @param remoteName the name of the git remote e.g. "origin".
+ */
+ void reset(Path repository, String branch, String remoteName);
+
+ /**
+ * Runs a git cleanup if untracked files were found.
+ *
+ * @param repository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where git
+ * will by default create a sub-folder by default on clone but the * final folder that will contain the ".git" subfolder.
+ */
+ void cleanup(Path repository);
+
+ /**
+ * Returns the URL of a git repository
+ *
+ * @param repository the {@link Path} to the folder where the git repository is located.
+ * @return the url of the repository as a {@link String}.
+ */
+ String retrieveGitUrl(Path repository);
+
+ /**
+ * @param repository the {@link Path} to the folder where the git repository is located.
+ * @return the name of the current branch.
+ */
+ String determineCurrentBranch(Path repository);
+
+ /**
+ * @param repository the {@link Path} to the folder where the git repository is located.
+ * @return the name of the default origin.
+ */
+ String determineRemote(Path repository);
+
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java
new file mode 100644
index 000000000..eda59a8e4
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java
@@ -0,0 +1,297 @@
+package com.devonfw.tools.ide.git;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import com.devonfw.tools.ide.cli.CliException;
+import com.devonfw.tools.ide.context.IdeContext;
+import com.devonfw.tools.ide.process.ProcessContext;
+import com.devonfw.tools.ide.process.ProcessErrorHandling;
+import com.devonfw.tools.ide.process.ProcessMode;
+import com.devonfw.tools.ide.process.ProcessResult;
+import com.devonfw.tools.ide.variable.IdeVariables;
+
+/**
+ * Implements the {@link GitContext}.
+ */
+public class GitContextImpl implements GitContext {
+
+ private final IdeContext context;
+
+ /**
+ * @param context the {@link IdeContext context}.
+ */
+ public GitContextImpl(IdeContext context) {
+
+ this.context = context;
+ }
+
+ @Override
+ public void pullOrCloneIfNeeded(GitUrl gitUrl, Path repository) {
+
+ GitOperation.PULL_OR_CLONE.executeIfNeeded(this.context, gitUrl, repository, null);
+ }
+
+ @Override
+ public boolean fetchIfNeeded(Path repository) {
+
+ return fetchIfNeeded(repository, null, null);
+ }
+
+ @Override
+ public boolean fetchIfNeeded(Path repository, String remote, String branch) {
+
+ return GitOperation.FETCH.executeIfNeeded(this.context, new GitUrl("https://dummy.url/repo.git", branch), repository, remote);
+ }
+
+ @Override
+ public boolean isRepositoryUpdateAvailable(Path repository) {
+
+ verifyGitInstalled();
+ String localCommitId = runGitCommandAndGetSingleOutput("Failed to get the local commit id.", repository, "rev-parse", "HEAD");
+ String remoteCommitId = runGitCommandAndGetSingleOutput("Failed to get the remote commit id.", repository, "rev-parse", "@{u}");
+ if ((localCommitId == null) || (remoteCommitId == null)) {
+ return false;
+ }
+ return !localCommitId.equals(remoteCommitId);
+ }
+
+ @Override
+ public void pullOrCloneAndResetIfNeeded(GitUrl gitUrl, Path repository, String remoteName) {
+
+ pullOrCloneIfNeeded(gitUrl, repository);
+ reset(repository, gitUrl.branch(), remoteName);
+ cleanup(repository);
+ }
+
+ @Override
+ public void pullOrClone(GitUrl gitUrl, Path repository) {
+
+ Objects.requireNonNull(repository);
+ Objects.requireNonNull(gitUrl);
+ if (Files.isDirectory(repository.resolve(GIT_FOLDER))) {
+ // checks for remotes
+ String remote = determineRemote(repository);
+ if (remote == null) {
+ String message = repository + " is a local git repository with no remote - if you did this for testing, you may continue...\n"
+ + "Do you want to ignore the problem and continue anyhow?";
+ this.context.askToContinue(message);
+ } else {
+ pull(repository);
+ }
+ } else {
+ clone(gitUrl, repository);
+ }
+ }
+
+ /**
+ * Handles errors which occurred during git pull.
+ *
+ * @param targetRepository the {@link Path} to the target folder where the git repository should be cloned or pulled. It is not the parent directory where
+ * git will by default create a sub-folder by default on clone but the * final folder that will contain the ".git" subfolder.
+ * @param result the {@link ProcessResult} to evaluate.
+ */
+ private void handleErrors(Path targetRepository, ProcessResult result) {
+
+ if (!result.isSuccessful()) {
+ String message = "Failed to update git repository at " + targetRepository;
+ if (this.context.isOffline()) {
+ this.context.warning(message);
+ this.context.interaction("Continuing as we are in offline mode - results may be outdated!");
+ } else {
+ this.context.error(message);
+ if (this.context.isOnline()) {
+ this.context.error("See above error for details. If you have local changes, please stash or revert and retry.");
+ } else {
+ this.context.error("It seems you are offline - please ensure Internet connectivity and retry or activate offline mode (-o or --offline).");
+ }
+ this.context.askToContinue("Typically you should abort and fix the problem. Do you want to continue anyways?");
+ }
+ }
+ }
+
+ @Override
+ public void clone(GitUrl gitUrl, Path repository) {
+
+ verifyGitInstalled();
+ GitUrlSyntax gitUrlSyntax = IdeVariables.PREFERRED_GIT_PROTOCOL.get(getContext());
+ gitUrl = gitUrlSyntax.format(gitUrl);
+ if (this.context.isOfflineMode()) {
+ this.context.requireOnline("git clone of " + gitUrl);
+ }
+ this.context.getFileAccess().mkdirs(repository);
+ List args = new ArrayList<>(7);
+ args.add("clone");
+ if (this.context.isQuietMode()) {
+ args.add("-q");
+ }
+ args.add("--recursive");
+ args.add(gitUrl.url());
+ args.add("--config");
+ args.add("core.autocrlf=false");
+ args.add(".");
+ runGitCommand(repository, args);
+ String branch = gitUrl.branch();
+ if (branch != null) {
+ runGitCommand(repository, "switch", branch);
+ }
+ }
+
+ @Override
+ public void pull(Path repository) {
+
+ verifyGitInstalled();
+ if (this.context.isOffline()) {
+ this.context.info("Skipping git pull on {} because offline", repository);
+ return;
+ }
+ ProcessResult result = runGitCommand(repository, ProcessMode.DEFAULT, "--no-pager", "pull", "--quiet");
+ if (!result.isSuccessful()) {
+ String branchName = determineCurrentBranch(repository);
+ this.context.warning("Git pull on branch {} failed for repository {}.", branchName, repository);
+ handleErrors(repository, result);
+ }
+ }
+
+ @Override
+ public void fetch(Path repository, String remote, String branch) {
+
+ verifyGitInstalled();
+ if (branch == null) {
+ branch = determineCurrentBranch(repository);
+ }
+ if (remote == null) {
+ remote = determineRemote(repository);
+ }
+
+ ProcessResult result = runGitCommand(repository, ProcessMode.DEFAULT_CAPTURE, "fetch", Objects.requireNonNullElse(remote, "origin"), branch);
+
+ if (!result.isSuccessful()) {
+ this.context.warning("Git fetch for '{}/{} failed.'.", remote, branch);
+ }
+ }
+
+ @Override
+ public String determineCurrentBranch(Path repository) {
+
+ verifyGitInstalled();
+ return runGitCommandAndGetSingleOutput("Failed to determine current branch of git repository", repository, "branch", "--show-current");
+ }
+
+ @Override
+ public String determineRemote(Path repository) {
+
+ verifyGitInstalled();
+ return runGitCommandAndGetSingleOutput("Failed to determine current origin of git repository.", repository, "remote");
+ }
+
+ @Override
+ public void reset(Path repository, String branchName, String remoteName) {
+
+ verifyGitInstalled();
+ if ((remoteName == null) || remoteName.isEmpty()) {
+ remoteName = DEFAULT_REMOTE;
+ }
+ ProcessResult result = runGitCommand(repository, ProcessMode.DEFAULT, "diff-index", "--quiet", "HEAD");
+ if (!result.isSuccessful()) {
+ // reset to origin/master
+ this.context.warning("Git has detected modified files -- attempting to reset {} to '{}/{}'.", repository, remoteName, branchName);
+ result = runGitCommand(repository, ProcessMode.DEFAULT, "reset", "--hard", remoteName + "/" + branchName);
+ if (!result.isSuccessful()) {
+ this.context.warning("Git failed to reset {} to '{}/{}'.", remoteName, branchName, repository);
+ handleErrors(repository, result);
+ }
+ }
+ }
+
+ @Override
+ public void cleanup(Path repository) {
+
+ verifyGitInstalled();
+ // check for untracked files
+ ProcessResult result = runGitCommand(repository, ProcessMode.DEFAULT_CAPTURE, "ls-files", "--other", "--directory", "--exclude-standard");
+ if (!result.getOut().isEmpty()) {
+ // delete untracked files
+ this.context.warning("Git detected untracked files in {} and is attempting a cleanup.", repository);
+ runGitCommand(repository, "clean", "-df");
+ }
+ }
+
+ @Override
+ public String retrieveGitUrl(Path repository) {
+
+ verifyGitInstalled();
+ return runGitCommandAndGetSingleOutput("Failed to retrieve git URL for repository", repository, "config", "--get", "remote.origin.url");
+ }
+
+ IdeContext getContext() {
+
+ return this.context;
+ }
+
+ /**
+ * Checks if there is a git installation and throws an exception if there is none
+ */
+ private void verifyGitInstalled() {
+
+ this.context.findBashRequired();
+ Path git = Path.of("git");
+ Path binaryGitPath = this.context.getPath().findBinary(git);
+ if (git == binaryGitPath) {
+ String message = "Could not find a git installation. We highly recommend installing git since most of our actions require git to work properly!";
+ throw new CliException(message);
+ }
+ this.context.trace("Git is installed");
+ }
+
+ private void runGitCommand(Path directory, String... args) {
+
+ ProcessResult result = runGitCommand(directory, ProcessMode.DEFAULT, args);
+ if (!result.isSuccessful()) {
+ String command = result.getCommand();
+ this.context.requireOnline(command);
+ result.failOnError();
+ }
+ }
+
+ private void runGitCommand(Path directory, List args) {
+
+ runGitCommand(directory, args.toArray(String[]::new));
+ }
+
+ private String runGitCommandAndGetSingleOutput(String warningOnError, Path directory, String... args) {
+
+ ProcessResult result = runGitCommand(directory, ProcessMode.DEFAULT_CAPTURE, args);
+ if (result.isSuccessful()) {
+ List out = result.getOut();
+ int size = out.size();
+ if (size == 1) {
+ return out.get(0);
+ } else if (size == 0) {
+ warningOnError += " - No output received from " + result.getCommand();
+ } else {
+ warningOnError += " - Expected single line of output but received " + size + " lines from " + result.getCommand();
+ }
+ }
+ this.context.warning(warningOnError);
+ return null;
+ }
+
+ private ProcessResult runGitCommand(Path directory, ProcessMode mode, String... args) {
+
+ return runGitCommand(directory, mode, ProcessErrorHandling.LOG_WARNING, args);
+ }
+
+ private ProcessResult runGitCommand(Path directory, ProcessMode mode, ProcessErrorHandling errorHandling, String... args) {
+
+ ProcessContext processContext = this.context.newProcess().executable("git").withEnvVar("GIT_TERMINAL_PROMPT", "0").errorHandling(errorHandling)
+ .directory(directory);
+ processContext.addArgs(args);
+ return processContext.run(mode);
+ }
+}
+
+
diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/GitOperation.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitOperation.java
similarity index 83%
rename from cli/src/main/java/com/devonfw/tools/ide/context/GitOperation.java
rename to cli/src/main/java/com/devonfw/tools/ide/git/GitOperation.java
index 38b3f0f15..b6cc4b35d 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/context/GitOperation.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitOperation.java
@@ -1,10 +1,12 @@
-package com.devonfw.tools.ide.context;
+package com.devonfw.tools.ide.git;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
+import com.devonfw.tools.ide.context.IdeContext;
+
/**
* An {@link Enum} for specific Git operations where we add caching support.
*
@@ -12,22 +14,24 @@
*/
public enum GitOperation {
+ /** {@link GitOperation} for {@link GitContext#fetch(Path, String, String)}. */
FETCH("fetch", "FETCH_HEAD", Duration.ofMinutes(5)) {
@Override
- protected boolean execute(IdeContext context, String gitRepoUrl, Path targetRepository, String remote, String branch) {
+ protected boolean execute(IdeContext context, GitUrl gitUrl, Path targetRepository, String remote) {
- context.getGitContext().fetch(targetRepository, remote, branch);
+ context.getGitContext().fetch(targetRepository, remote, gitUrl.branch());
// TODO: see JavaDoc, implementation incorrect. fetch needs to return boolean if changes have been fetched
// and then this result must be returned - or JavaDoc needs to changed
return true;
}
},
+ /** {@link GitOperation} for {@link GitContext#clone(GitUrl, Path)}. */
PULL_OR_CLONE("pull/clone", "HEAD", Duration.ofMinutes(30)) {
@Override
- protected boolean execute(IdeContext context, String gitRepoUrl, Path targetRepository, String remote, String branch) {
+ protected boolean execute(IdeContext context, GitUrl gitUrl, Path targetRepository, String remote) {
- context.getGitContext().pullOrClone(gitRepoUrl, targetRepository, branch);
+ context.getGitContext().pullOrClone(gitUrl, targetRepository);
return true;
}
};
@@ -99,28 +103,26 @@ public boolean isNeededIfGitFolderNotPresent() {
* Executes this {@link GitOperation} physically.
*
* @param context the {@link IdeContext}.
- * @param gitRepoUrl the git repository URL. Maybe {@code null} if not required by the operation.
+ * @param gitUrl the git repository URL. Maybe {@code null} if not required by the operation.
* @param targetRepository the {@link Path} to the git repository.
* @param remote the git remote (e.g. "origin"). Maybe {@code null} if not required by the operation.
- * @param branch the explicit git branch (e.g. "main"). Maybe {@code null} for default branch or if not required by the operation.
* @return {@code true} if changes were received from git, {@code false} otherwise.
*/
- protected abstract boolean execute(IdeContext context, String gitRepoUrl, Path targetRepository, String remote, String branch);
+ protected abstract boolean execute(IdeContext context, GitUrl gitUrl, Path targetRepository, String remote);
/**
* Executes this {@link GitOperation} if {@link #isNeeded(Path, IdeContext) needed}.
*
* @param context the {@link IdeContext}.
- * @param gitRepoUrl the git repository URL. Maybe {@code null} if not required by the operation.
+ * @param gitUrl the git repository URL. Maybe {@code null} if not required by the operation.
* @param targetRepository the {@link Path} to the git repository.
* @param remote the git remote (e.g. "origin"). Maybe {@code null} if not required by the operation.
- * @param branch the explicit git branch (e.g. "main"). Maybe {@code null} for default branch or if not required by the operation.
* @return {@code true} if changes were received from git, {@code false} otherwise (e.g. no git operation was invoked at all).
*/
- boolean executeIfNeeded(IdeContext context, String gitRepoUrl, Path targetRepository, String remote, String branch) {
+ boolean executeIfNeeded(IdeContext context, GitUrl gitUrl, Path targetRepository, String remote) {
if (isNeeded(targetRepository, context)) {
- boolean result = execute(context, gitRepoUrl, targetRepository, remote, branch);
+ boolean result = execute(context, gitUrl, targetRepository, remote);
if (isForceUpdateTimestampFile()) {
Path timestampPath = targetRepository.resolve(GitContext.GIT_FOLDER).resolve(this.timestampFilename);
try {
diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitUrl.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitUrl.java
new file mode 100644
index 000000000..2245f155d
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitUrl.java
@@ -0,0 +1,79 @@
+package com.devonfw.tools.ide.git;
+
+/**
+ * Handles parsing of git URLs.
+ *
+ * @param url the git url e.g. https://github.com/devonfw/ide-urls.git.
+ * @param branch the branch name e.g. master.
+ */
+public record GitUrl(String url, String branch) {
+
+ /** {@link #branch() Branch} @{@value }. */
+ public static final String BRANCH_MAIN = "main";
+
+ /** {@link #branch() Branch} @{@value }. */
+ public static final String BRANCH_MASTER = "master";
+
+ /**
+ * The constructor.
+ */
+ public GitUrl {
+ if (url.contains("#")) {
+ String message = "Invalid git URL " + url;
+ assert false : message;
+ }
+ }
+
+ /**
+ * Converts the Git URL based on the specified {@link GitUrlSyntax}.
+ *
+ * @param syntax the preferred {@link GitUrlSyntax} (SSH or HTTPS).
+ * @return the converted {@link GitUrl} or the original if no conversion is required.
+ */
+ public GitUrl convert(GitUrlSyntax syntax) {
+ return syntax.format(this);
+ }
+
+ @Override
+ public String toString() {
+
+ if (this.branch == null) {
+ return this.url;
+ }
+ return this.url + "#" + this.branch;
+ }
+
+ /**
+ * @param gitUrl the {@link #toString() string representation} of a {@link GitUrl}. May contain a branch name as {@code «url»#«branch»}.
+ * @return the parsed {@link GitUrl}.
+ */
+ public static GitUrl of(String gitUrl) {
+
+ int hashIndex = gitUrl.indexOf('#');
+ String url = gitUrl;
+ String branch = null;
+ if (hashIndex > 0) {
+ url = gitUrl.substring(0, hashIndex);
+ branch = gitUrl.substring(hashIndex + 1);
+ }
+ return new GitUrl(url, branch);
+ }
+
+ /**
+ * @param gitUrl the git {@link #url() URL}.
+ * @return a new instance of {@link GitUrl} with the given URL and {@link #BRANCH_MAIN}.
+ */
+ public static GitUrl ofMain(String gitUrl) {
+
+ return new GitUrl(gitUrl, BRANCH_MAIN);
+ }
+
+ /**
+ * @param gitUrl the git {@link #url() URL}.
+ * @return a new instance of {@link GitUrl} with the given URL and {@link #BRANCH_MASTER}.
+ */
+ public static GitUrl ofMaster(String gitUrl) {
+
+ return new GitUrl(gitUrl, BRANCH_MASTER);
+ }
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/GitUrlSyntax.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitUrlSyntax.java
similarity index 55%
rename from cli/src/main/java/com/devonfw/tools/ide/context/GitUrlSyntax.java
rename to cli/src/main/java/com/devonfw/tools/ide/git/GitUrlSyntax.java
index 81f2dc50d..38f0d60e4 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/context/GitUrlSyntax.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitUrlSyntax.java
@@ -1,4 +1,4 @@
-package com.devonfw.tools.ide.context;
+package com.devonfw.tools.ide.git;
import java.util.Arrays;
import java.util.List;
@@ -11,16 +11,51 @@ public enum GitUrlSyntax {
/**
* The DEFAULT Git URL syntax
*/
- DEFAULT(""),
+ DEFAULT("") {
+ @Override
+ public GitUrl format(GitUrl gitUrl) {
+ return gitUrl; // No conversion for DEFAULT
+ }
+ },
/**
* The SSH Git URL syntax (e.g., git@github.com:user/repo.git).
*/
- SSH("git@"),
+ SSH("git@") {
+ @Override
+ public GitUrl format(GitUrl gitUrl) {
+ String url = gitUrl.url();
+ if (isDomainWithNoConversion(url.toLowerCase())) {
+ return gitUrl;
+ }
+ if (url.startsWith(HTTPS.prefix)) {
+ int index = url.indexOf("/", HTTPS.prefix.length());
+ if (index > 0) {
+ url = SSH.prefix + url.substring(HTTPS.prefix.length(), index) + ":" + url.substring(index + 1);
+ }
+ }
+ return new GitUrl(url, gitUrl.branch());
+ }
+ },
/**
* The HTTPS Git URL syntax (e.g., https://github.com/user/repo.git).
*/
- HTTPS("https://");
+ HTTPS("https://") {
+ @Override
+ public GitUrl format(GitUrl gitUrl) {
+ String url = gitUrl.url();
+ if (isDomainWithNoConversion(url.toLowerCase())) {
+ return gitUrl;
+ }
+ if (url.startsWith(SSH.prefix)) {
+ int index = url.indexOf(":");
+ if (index > 0) {
+ url = HTTPS.prefix + url.substring(SSH.prefix.length(), index) + "/" + url.substring(index + 1);
+ }
+ }
+ return new GitUrl(url, gitUrl.branch());
+ }
+ };
private final String prefix;
@@ -40,41 +75,9 @@ public enum GitUrlSyntax {
* @return the formatted {@link GitUrl} according to this syntax.
* @throws IllegalArgumentException if the protocol is not supported.
*/
- public GitUrl format(GitUrl gitUrl) {
- if (this == DEFAULT) {
- return gitUrl;
- }
- String url = gitUrl.url();
-
- // Prevent conversion for domains in the no-conversion list
- if (isDomainWithNoConversion(url.toLowerCase())) {
- return gitUrl;
- }
-
- switch (this) {
- case SSH -> {
- if (url.startsWith(HTTPS.prefix)) {
- int index = url.indexOf("/", HTTPS.prefix.length());
- if (index > 0) {
- url = SSH.prefix + url.substring(HTTPS.prefix.length(), index) + ":" + url.substring(index + 1);
- }
- }
- }
- case HTTPS -> {
- if (url.startsWith(SSH.prefix)) {
- int index = url.indexOf(":");
- if (index > 0) {
- url = HTTPS.prefix + url.substring(SSH.prefix.length(), index) + "/" + url.substring(index + 1);
- }
- }
- }
- default -> throw new IllegalArgumentException("Unsupported protocol: " + this);
- }
-
- return new GitUrl(url, gitUrl.branch());
- }
+ public abstract GitUrl format(GitUrl gitUrl);
- private boolean isDomainWithNoConversion(String url) {
+ private static boolean isDomainWithNoConversion(String url) {
for (String domain : DOMAINS_WITH_NO_CONVERSION) {
// Check if it's an HTTPS URL for the domain
diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/AbstractIdeProgressBar.java b/cli/src/main/java/com/devonfw/tools/ide/io/AbstractIdeProgressBar.java
index ad425cce8..14682e9b2 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/io/AbstractIdeProgressBar.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/io/AbstractIdeProgressBar.java
@@ -5,22 +5,59 @@
*/
public abstract class AbstractIdeProgressBar implements IdeProgressBar {
- private long currentProgress;
+ /** @see #getTitle() */
+ protected final String title;
+
+ /** @see #getMaxSize() */
+ protected final long maxSize;
+
+ /** @see #getUnitName() */
+ protected final String unitName;
- private final long maxLength;
+ /** @see #getUnitSize() */
+ protected final long unitSize;
+
+ private long currentProgress;
/**
- * @param maxLength the maximum length of the progress bar.
+ * The constructor.
+ *
+ * @param title the {@link #getTitle() title}.
+ * @param maxSize the {@link #getMaxSize() maximum size}.
+ * @param unitName the {@link #getUnitName() unit name}.
+ * @param unitSize the {@link #getUnitSize() unit size}.
*/
- public AbstractIdeProgressBar(long maxLength) {
+ public AbstractIdeProgressBar(String title, long maxSize, String unitName, long unitSize) {
+
+ super();
+ this.title = title;
+ this.maxSize = maxSize;
+ this.unitName = unitName;
+ this.unitSize = unitSize;
+ }
+
+ @Override
+ public String getTitle() {
+
+ return this.title;
+ }
+
+ @Override
+ public long getMaxSize() {
+
+ return this.maxSize;
+ }
+
+ @Override
+ public String getUnitName() {
- this.maxLength = maxLength;
+ return this.unitName;
}
@Override
- public long getMaxLength() {
+ public long getUnitSize() {
- return maxLength;
+ return this.unitSize;
}
/**
@@ -38,8 +75,8 @@ public long getMaxLength() {
*/
protected void stepTo(long stepPosition) {
- if ((this.maxLength > 0) && (stepPosition > this.maxLength)) {
- stepPosition = this.maxLength; // clip to max avoiding overflow
+ if ((this.maxSize > 0) && (stepPosition > this.maxSize)) {
+ stepPosition = this.maxSize; // clip to max avoiding overflow
}
this.currentProgress = stepPosition;
doStepTo(stepPosition);
@@ -56,11 +93,11 @@ protected void stepTo(long stepPosition) {
public void stepBy(long stepSize) {
this.currentProgress += stepSize;
- if (this.maxLength > 0) {
+ if (this.maxSize > 0) {
// check if maximum overflow
- if (this.currentProgress > this.maxLength) {
- this.currentProgress = this.maxLength;
- stepTo(this.maxLength);
+ if (this.currentProgress > this.maxSize) {
+ this.currentProgress = this.maxSize;
+ stepTo(this.maxSize);
return;
}
}
@@ -76,12 +113,12 @@ public long getCurrentProgress() {
@Override
public void close() {
- if (this.maxLength < 0) {
+ if (this.maxSize < 0) {
return;
}
- if (this.currentProgress < this.maxLength) {
- stepTo(this.maxLength);
+ if (this.currentProgress < this.maxSize) {
+ stepTo(this.maxSize);
}
}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java
index a4ae5d3d0..d3f46e900 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java
@@ -90,7 +90,7 @@ default void symlink(Path source, Path targetLink) {
/**
* @param source the source {@link Path file or folder} to copy.
- * @param target the {@link Path} to copy {@code source} to. See {@link #copy(Path, Path, FileCopyMode)} for details. will always ensure that in the end
+ * @param target the {@link Path} to copy {@code source} to. See {@link #copy(Path, Path, FileCopyMode)} for details. Will always ensure that in the end
* you will find the same content of {@code source} in {@code target}.
*/
default void copy(Path source, Path target) {
@@ -105,10 +105,24 @@ default void copy(Path source, Path target) {
* {@code target}. Therefore the result is always clear and easy to predict and understand. Also you can easily rename a file to copy. While
* {@code cp my-file target} may lead to a different result than {@code cp my-file target/} this method will always ensure that in the end you will find
* the same content of {@code source} in {@code target}.
- * @param fileOnly - {@code true} if {@code fileOrFolder} is expected to be a file and an exception shall be thrown if it is a directory, {@code false}
- * otherwise (copy recursively).
+ * @param mode the {@link FileCopyMode}.
*/
- void copy(Path source, Path target, FileCopyMode fileOnly);
+ default void copy(Path source, Path target, FileCopyMode mode) {
+
+ copy(source, target, mode, PathCopyListener.NONE);
+ }
+
+ /**
+ * @param source the source {@link Path file or folder} to copy.
+ * @param target the {@link Path} to copy {@code source} to. Unlike the Linux {@code cp} command this method will not take the filename of {@code source}
+ * and copy that to {@code target} in case that is an existing folder. Instead it will always be simple and stupid and just copy from {@code source} to
+ * {@code target}. Therefore the result is always clear and easy to predict and understand. Also you can easily rename a file to copy. While
+ * {@code cp my-file target} may lead to a different result than {@code cp my-file target/} this method will always ensure that in the end you will find
+ * the same content of {@code source} in {@code target}.
+ * @param mode the {@link FileCopyMode}.
+ * @param listener the {@link PathCopyListener} that will be called for each copied {@link Path}.
+ */
+ void copy(Path source, Path target, FileCopyMode mode, PathCopyListener listener);
/**
* @param archiveFile the {@link Path} to the file to extract.
diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java
index 9860df712..763616e2a 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java
@@ -6,15 +6,14 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.net.InetSocketAddress;
-import java.net.Proxy;
-import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Redirect;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
+import java.nio.file.FileSystem;
import java.nio.file.FileSystemException;
+import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
@@ -30,15 +29,12 @@
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
-import java.util.jar.JarEntry;
-import java.util.jar.JarInputStream;
import java.util.stream.Stream;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
@@ -66,6 +62,8 @@ public class FileAccessImpl implements FileAccess {
"On Windows, file operations could fail due to file locks. Please ensure the files in the moved directory are not in use. For further details, see: \n"
+ WINDOWS_FILE_LOCK_DOCUMENTATION_PAGE;
+ private static final Map FS_ENV = Map.of("encoding", "UTF-8");
+
private final IdeContext context;
/**
@@ -82,12 +80,6 @@ public FileAccessImpl(IdeContext context) {
private HttpClient createHttpClient(String url) {
HttpClient.Builder builder = HttpClient.newBuilder().followRedirects(Redirect.ALWAYS);
- Proxy proxy = this.context.getProxyContext().getProxy(url);
- if (proxy != Proxy.NO_PROXY) {
- this.context.info("Downloading through proxy: " + proxy);
- InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
- builder.proxy(ProxySelector.of(proxyAddress));
- }
return builder.build();
}
@@ -105,7 +97,6 @@ public void download(String url, Path target) {
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build();
HttpClient client = createHttpClient(url);
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
-
if (response.statusCode() == 200) {
downloadFileWithProgressBar(url, target, response);
}
@@ -136,7 +127,7 @@ public void download(String url, Path target) {
private void downloadFileWithProgressBar(String url, Path target, HttpResponse response) {
long contentLength = response.headers().firstValueAsLong("content-length").orElse(-1);
- informAboutMissingContentLength(contentLength, url, null);
+ informAboutMissingContentLength(contentLength, url);
byte[] data = new byte[1024];
boolean fileComplete = false;
@@ -145,7 +136,7 @@ private void downloadFileWithProgressBar(String url, Path target, HttpResponse 0) {
out.write(buf, 0, readBytes);
if (size > 0) {
@@ -190,17 +177,10 @@ private void copyFileWithProgressBar(Path source, Path target) throws IOExceptio
}
}
- private void informAboutMissingContentLength(long contentLength, String url, Path path) {
+ private void informAboutMissingContentLength(long contentLength, String url) {
- String source;
if (contentLength < 0) {
- if (path != null) {
- source = path.toString();
- } else {
- source = url;
- }
- this.context.warning("Content-Length was not provided by download/copy source: {}.",
- source);
+ this.context.warning("Content-Length was not provided by download from {}", url);
}
}
@@ -257,8 +237,7 @@ public String checksum(Path file) {
throw new IllegalStateException("Failed to read and hash file " + file, e);
}
byte[] digestBytes = md.digest();
- String checksum = HexUtil.toHexString(digestBytes);
- return checksum;
+ return HexUtil.toHexString(digestBytes);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("No such hash algorithm " + UrlChecksum.HASH_ALGORITHM, e);
}
@@ -317,7 +296,7 @@ public void move(Path source, Path targetDir) {
}
@Override
- public void copy(Path source, Path target, FileCopyMode mode) {
+ public void copy(Path source, Path target, FileCopyMode mode, PathCopyListener listener) {
if (mode != FileCopyMode.COPY_TREE_CONTENT) {
// if we want to copy the file or folder "source" to the existing folder "target" in a shell this will copy
@@ -328,32 +307,40 @@ public void copy(Path source, Path target, FileCopyMode mode) {
// Therefore we need to add the filename (foldername) of "source" to the "target" path before.
// For the rare cases, where we want to copy the content of a folder (cp -r source/* target) we support
// it via the COPY_TREE_CONTENT mode.
- target = target.resolve(source.getFileName());
+ Path fileName = source.getFileName();
+ if (fileName != null) { // if filename is null, we are copying the root of a (virtual filesystem)
+ target = target.resolve(fileName.toString());
+ }
}
boolean fileOnly = mode.isFileOnly();
- if (fileOnly) {
- this.context.debug("Copying file {} to {}", source, target);
+ String operation = mode.getOperation();
+ if (mode.isExtract()) {
+ this.context.debug("Starting to {} to {}", operation, target);
} else {
- this.context.debug("Copying {} recursively to {}", source, target);
+ if (fileOnly) {
+ this.context.debug("Starting to {} file {} to {}", operation, source, target);
+ } else {
+ this.context.debug("Starting to {} {} recursively to {}", operation, source, target);
+ }
}
if (fileOnly && Files.isDirectory(source)) {
throw new IllegalStateException("Expected file but found a directory to copy at " + source);
}
if (mode.isFailIfExists()) {
if (Files.exists(target)) {
- throw new IllegalStateException("Failed to copy " + source + " to already existing target " + target);
+ throw new IllegalStateException("Failed to " + operation + " " + source + " to already existing target " + target);
}
} else if (mode == FileCopyMode.COPY_TREE_OVERRIDE_TREE) {
delete(target);
}
try {
- copyRecursive(source, target, mode);
+ copyRecursive(source, target, mode, listener);
} catch (IOException e) {
- throw new IllegalStateException("Failed to copy " + source + " to " + target, e);
+ throw new IllegalStateException("Failed to " + operation + " " + source + " to " + target, e);
}
}
- private void copyRecursive(Path source, Path target, FileCopyMode mode) throws IOException {
+ private void copyRecursive(Path source, Path target, FileCopyMode mode, PathCopyListener listener) throws IOException {
if (Files.isDirectory(source)) {
mkdirs(target);
@@ -361,15 +348,17 @@ private void copyRecursive(Path source, Path target, FileCopyMode mode) throws I
Iterator iterator = childStream.iterator();
while (iterator.hasNext()) {
Path child = iterator.next();
- copyRecursive(child, target.resolve(child.getFileName()), mode);
+ copyRecursive(child, target.resolve(child.getFileName().toString()), mode, listener);
}
}
+ listener.onCopy(source, target, true);
} else if (Files.exists(source)) {
if (mode.isOverrideFile()) {
delete(target);
}
- this.context.trace("Copying {} to {}", source, target);
+ this.context.trace("Starting to {} {} to {}", mode.getOperation(), source, target);
Files.copy(source, target);
+ listener.onCopy(source, target, false);
} else {
throw new IOException("Path " + source + " does not exist.");
}
@@ -564,24 +553,12 @@ public void extract(Path archiveFile, Path targetDir, Consumer postExtract
this.context.trace("Determined file extension {}", extension);
}
switch (extension) {
- case "zip" -> {
- extractZip(archiveFile, tmpDir);
- }
- case "jar" -> {
- extractJar(archiveFile, tmpDir);
- }
- case "dmg" -> {
- extractDmg(archiveFile, tmpDir);
- }
- case "msi" -> {
- extractMsi(archiveFile, tmpDir);
- }
- case "pkg" -> {
- extractPkg(archiveFile, tmpDir);
- }
- default -> {
- throw new IllegalStateException("Unknown archive format " + extension + ". Can not extract " + archiveFile);
- }
+ case "zip" -> extractZip(archiveFile, tmpDir);
+ case "jar" -> extractJar(archiveFile, tmpDir);
+ case "dmg" -> extractDmg(archiveFile, tmpDir);
+ case "msi" -> extractMsi(archiveFile, tmpDir);
+ case "pkg" -> extractPkg(archiveFile, tmpDir);
+ default -> throw new IllegalStateException("Unknown archive format " + extension + ". Can not extract " + archiveFile);
}
}
Path properInstallDir = getProperInstallationSubDirOf(tmpDir, archiveFile);
@@ -624,7 +601,37 @@ private Path getProperInstallationSubDirOf(Path path, Path archiveFile) {
@Override
public void extractZip(Path file, Path targetDir) {
- extractZipArchive(file, targetDir);
+ this.context.info("Extracting ZIP file {} to {}", file, targetDir);
+ URI uri = URI.create("jar:" + file.toUri());
+ try (FileSystem fs = FileSystems.newFileSystem(uri, FS_ENV)) {
+ long size = 0;
+ for (Path root : fs.getRootDirectories()) {
+ size += getFileSizeRecursive(root);
+ }
+ try (IdeProgressBar bp = this.context.newProgressbarForExtracting(size)) {
+ PathCopyListener listener = (source, target, directory) -> {
+ if (directory) {
+ return;
+ }
+ if (!context.getSystemInfo().isWindows()) {
+ try {
+ Object attribute = Files.getAttribute(source, "zip:permissions");
+ if (attribute instanceof Set> permissionSet) {
+ Files.setPosixFilePermissions(target, (Set) permissionSet);
+ }
+ } catch (Exception e) {
+ context.error(e, "Failed to transfer zip permissions for {}", target);
+ }
+ }
+ bp.stepBy(getFileSize(target));
+ };
+ for (Path root : fs.getRootDirectories()) {
+ copy(root, targetDir, FileCopyMode.EXTRACT, listener);
+ }
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to extract " + file + " to " + targetDir, e);
+ }
}
@Override
@@ -636,29 +643,7 @@ public void extractTar(Path file, Path targetDir, TarCompression compression) {
@Override
public void extractJar(Path file, Path targetDir) {
- this.context.trace("Unpacking JAR {} to {}", file, targetDir);
- try (JarInputStream jis = new JarInputStream(Files.newInputStream(file)); IdeProgressBar pb = getProgressbarForUnpacking(
- getFileSize(file))) {
- JarEntry entry;
- while ((entry = jis.getNextJarEntry()) != null) {
- Path entryPath = targetDir.resolve(entry.getName()).toAbsolutePath();
-
- if (!entryPath.startsWith(targetDir)) {
- throw new IOException("Preventing path traversal attack from " + entry.getName() + " to " + entryPath);
- }
-
- if (entry.isDirectory()) {
- Files.createDirectories(entryPath);
- } else {
- Files.createDirectories(entryPath.getParent());
- Files.copy(jis, entryPath);
- }
- pb.stepBy(entry.getCompressedSize());
- jis.closeEntry();
- }
- } catch (IOException e) {
- throw new IllegalStateException("Failed to extract JAR " + file + " to " + targetDir, e);
- }
+ extractZip(file, targetDir);
}
/**
@@ -671,7 +656,6 @@ public static String generatePermissionString(int permissions) {
permissions &= 0b111111111;
StringBuilder permissionStringBuilder = new StringBuilder("rwxrwxrwx");
-
for (int i = 0; i < 9; i++) {
int mask = 1 << i;
char currentChar = ((permissions & mask) != 0) ? permissionStringBuilder.charAt(8 - i) : '-';
@@ -681,11 +665,13 @@ public static String generatePermissionString(int permissions) {
return permissionStringBuilder.toString();
}
- private void extractArchive(Path file, Path targetDir, Function unpacker) {
+ private void extractArchive(Path file, Path targetDir, Function> unpacker) {
+
+ this.context.info("Extracting TAR file {} to {}", file, targetDir);
+ try (InputStream is = Files.newInputStream(file);
+ ArchiveInputStream> ais = unpacker.apply(is);
+ IdeProgressBar pb = this.context.newProgressbarForExtracting(getFileSize(file))) {
- this.context.trace("Unpacking archive {} to {}", file, targetDir);
- try (InputStream is = Files.newInputStream(file); ArchiveInputStream ais = unpacker.apply(is); IdeProgressBar pb = getProgressbarForUnpacking(
- getFileSize(file))) {
ArchiveEntry entry = ais.getNextEntry();
boolean isTar = ais instanceof TarArchiveInputStream;
while (entry != null) {
@@ -718,37 +704,10 @@ private void extractArchive(Path file, Path targetDir, Function filter, boolean recurs
return null;
}
- /**
- * @param sizeFile the size of archive
- * @return prepared progressbar for unpacking
- */
- private IdeProgressBar getProgressbarForUnpacking(long sizeFile) {
-
- return this.context.prepareProgressBar("Unpacking", sizeFile);
- }
-
@Override
public List listChildren(Path dir, Predicate filter) {
@@ -902,15 +854,33 @@ public boolean isEmptyDir(Path dir) {
return listChildren(dir, f -> true).isEmpty();
}
- /**
- * Gets the file size of a provided file path.
- *
- * @param path of the file.
- * @return the file size.
- */
- protected long getFileSize(Path path) {
+ private long getFileSize(Path file) {
- return path.toFile().length();
+ try {
+ return Files.size(file);
+ } catch (IOException e) {
+ this.context.warning(e.getMessage(), e);
+ return 0;
+ }
+ }
+
+ private long getFileSizeRecursive(Path path) {
+
+ long size = 0;
+ if (Files.isDirectory(path)) {
+ try (Stream childStream = Files.list(path)) {
+ Iterator iterator = childStream.iterator();
+ while (iterator.hasNext()) {
+ Path child = iterator.next();
+ size += getFileSizeRecursive(child);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to iterate children of folder " + path, e);
+ }
+ } else {
+ size += getFileSize(path);
+ }
+ return size;
}
@Override
@@ -931,31 +901,31 @@ public Path findExistingFile(String fileName, List searchDirs) {
@Override
public void makeExecutable(Path filePath) {
- if (SystemInfoImpl.INSTANCE.isWindows()) {
- return;
- }
if (Files.exists(filePath)) {
- // Read the current file permissions
- Set perms;
- try {
- perms = Files.getPosixFilePermissions(filePath);
- } catch (IOException e) {
- throw new RuntimeException(e);
+ if (SystemInfoImpl.INSTANCE.isWindows()) {
+ this.context.trace("Windows does not have executable flags hence omitting for file {}", filePath);
+ return;
}
+ try {
+ // Read the current file permissions
+ Set perms = Files.getPosixFilePermissions(filePath);
- if (perms != null) {
// Add execute permission for all users
- perms.add(PosixFilePermission.OWNER_EXECUTE);
- perms.add(PosixFilePermission.GROUP_EXECUTE);
- perms.add(PosixFilePermission.OTHERS_EXECUTE);
-
- // Set the new permissions
- try {
+ boolean update = false;
+ update |= perms.add(PosixFilePermission.OWNER_EXECUTE);
+ update |= perms.add(PosixFilePermission.GROUP_EXECUTE);
+ update |= perms.add(PosixFilePermission.OTHERS_EXECUTE);
+
+ if (update) {
+ this.context.debug("Setting executable flags for file {}", filePath);
+ // Set the new permissions
Files.setPosixFilePermissions(filePath, perms);
- } catch (IOException e) {
- throw new RuntimeException(e);
+ } else {
+ this.context.trace("Executable flags already present so no need to set them for file {}", filePath);
}
+ } catch (IOException e) {
+ throw new RuntimeException(e);
}
} else {
this.context.warning("Cannot set executable flag on file that does not exist: {}", filePath);
diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/FileCopyMode.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileCopyMode.java
index cbf8d32bf..08686cbc2 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/io/FileCopyMode.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileCopyMode.java
@@ -19,16 +19,16 @@ public enum FileCopyMode {
/** Copy {@link #isRecursive() recursively} and override existing files but merge existing folders. */
COPY_TREE_OVERRIDE_FILES,
- /**
- * Copy {@link #isRecursive() recursively} and {@link FileAccess#delete(java.nio.file.Path) delete} the target-file if it exists before copying.
- */
+ /** Copy {@link #isRecursive() recursively} from virtual filesystem of a compressed archive and override existing files but merge existing folders. */
+ EXTRACT,
+
+ /** Copy {@link #isRecursive() recursively} and {@link FileAccess#delete(java.nio.file.Path) delete} the target-file if it exists before copying. */
COPY_TREE_OVERRIDE_TREE,
- /**
- * Copy {@link #isRecursive() recursively} and appends the file name to the target.
- */
+ /** Copy {@link #isRecursive() recursively} and append the file name to the target. */
COPY_TREE_CONTENT;
+
/**
* @return {@code true} if only a single file shall be copied. Will fail if a directory is given to copy, {@code false} otherwise (to copy folders
* recursively).
@@ -54,8 +54,30 @@ public boolean isFailIfExists() {
return (this == COPY_FILE_FAIL_IF_EXISTS) || (this == COPY_TREE_FAIL_IF_EXISTS);
}
+ /**
+ * @return {@code true} to override existing files, {@code false} otherwise.
+ */
public boolean isOverrideFile() {
return (this == COPY_FILE_OVERRIDE) || (this == COPY_TREE_OVERRIDE_FILES);
}
+
+ /**
+ * @return {@code true} if we copy from a virtual filesystem of a compressed archive.
+ */
+ public boolean isExtract() {
+
+ return (this == EXTRACT);
+ }
+
+ /**
+ * @return the name of the operation (typically "copy" but may also be e.g. "extract").
+ */
+ public String getOperation() {
+
+ if (isExtract()) {
+ return "extract";
+ }
+ return "copy";
+ }
}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/IdeProgressBar.java b/cli/src/main/java/com/devonfw/tools/ide/io/IdeProgressBar.java
index 2370ca497..6d6612265 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/io/IdeProgressBar.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/io/IdeProgressBar.java
@@ -5,6 +5,36 @@
*/
public interface IdeProgressBar extends AutoCloseable {
+ /** The {@link #getTitle() title} for extracting. */
+ String TITLE_EXTRACTING = "Extracting";
+
+ /** The {@link #getTitle() title} for downloading. */
+ String TITLE_DOWNLOADING = "Downloading";
+
+ /** The {@link #getTitle() title} for copying. */
+ String TITLE_COPYING = "Copying";
+
+ /**
+ * @return the title (task name or activity) to display in the progress bar.
+ */
+ String getTitle();
+
+ /**
+ * @return the maximum value when the progress bar has reached its end (100%) or {@code -1} if the maximum is undefined.
+ */
+ long getMaxSize();
+
+ /**
+ * @return the name of the unit displayed to the end user (e.g. "files" or "MiB").
+ */
+ String getUnitName();
+
+ /**
+ * @return the size of a single unit (e.g. 1 if the {@link #stepBy(long) reported progress} and {@link #getMaxSize() max size} numbers remain unchanged or 1000 for
+ * "kilo" or 1000000 for "mega" in order to avoid displaying too long numbers).
+ */
+ long getUnitSize();
+
/**
* Increases the progress bar by given step size.
*
@@ -13,12 +43,7 @@ public interface IdeProgressBar extends AutoCloseable {
void stepBy(long stepSize);
/**
- * @return the maximum value when the progress bar has reached its end or {@code -1} if the maximum is undefined.
- */
- long getMaxLength();
-
- /**
- * @return the total count accumulated with {@link #stepBy(long)} or {@link #getMaxLength()} in case of overflow.
+ * @return the total count accumulated with {@link #stepBy(long)} or {@link #getMaxSize()} in case of overflow.
*/
long getCurrentProgress();
diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/IdeProgressBarConsole.java b/cli/src/main/java/com/devonfw/tools/ide/io/IdeProgressBarConsole.java
index d89a74c55..9223f6239 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/io/IdeProgressBarConsole.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/io/IdeProgressBarConsole.java
@@ -18,15 +18,16 @@ public class IdeProgressBarConsole extends AbstractIdeProgressBar {
* The constructor.
*
* @param systemInfo the {@link SystemInfo}.
- * @param taskName the {@link ProgressBar} to initialize.
+ * @param title the title (task name or activity) to display in the progress bar.
* @param maxSize the maximum size of the progress bar.
+ * @param unitName the name of the unit to display in the progress bar.
+ * @param unitSize the size of the unit (e.g. 1000 for kilo, 1000000 for mega).
*/
- public IdeProgressBarConsole(SystemInfo systemInfo, String taskName, long maxSize) {
-
- super(maxSize);
+ public IdeProgressBarConsole(SystemInfo systemInfo, String title, long maxSize, String unitName, long unitSize) {
+ super(title, maxSize, unitName, unitSize);
this.systemInfo = systemInfo;
- this.progressBar = createProgressBar(taskName, maxSize);
+ this.progressBar = createProgressBar();
}
/**
@@ -34,17 +35,10 @@ public IdeProgressBarConsole(SystemInfo systemInfo, String taskName, long maxSiz
*/
protected ProgressBar getProgressBar() {
- return progressBar;
+ return this.progressBar;
}
- /**
- * Creates the {@link ProgressBar} initializes task name and maximum size as well as the behaviour and style.
- *
- * @param taskName name of the task.
- * @param size of the content.
- * @return {@link ProgressBar} to use.
- */
- protected ProgressBar createProgressBar(String taskName, long size) {
+ private ProgressBar createProgressBar() {
ProgressBarBuilder pbb = new ProgressBarBuilder();
String leftBracket, rightBracket, fractionSymbols;
@@ -66,18 +60,18 @@ protected ProgressBar createProgressBar(String taskName, long size) {
.rightBracket(rightBracket).block(block).space(' ').fractionSymbols(fractionSymbols).rightSideFractionSymbol(' ')
.build());
- pbb.setUnit("MiB", 1048576);
- if (size <= 0) {
- pbb.setTaskName(taskName + " (unknown size)");
+ pbb.setUnit(this.unitName, this.unitSize);
+ if (this.maxSize <= 0) {
+ pbb.setTaskName(this.title + " (unknown size)");
pbb.setInitialMax(-1);
pbb.hideEta();
} else {
- pbb.setTaskName(taskName);
+ pbb.setTaskName(this.title);
pbb.showSpeed();
- pbb.setInitialMax(size);
+ pbb.setInitialMax(this.maxSize);
}
pbb.continuousUpdate();
- pbb.setUpdateIntervalMillis(1);
+ pbb.setUpdateIntervalMillis(100);
return pbb.build();
}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/PathCopyListener.java b/cli/src/main/java/com/devonfw/tools/ide/io/PathCopyListener.java
new file mode 100644
index 000000000..442e3abe9
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/io/PathCopyListener.java
@@ -0,0 +1,27 @@
+package com.devonfw.tools.ide.io;
+
+import java.nio.file.Path;
+
+/**
+ * Interface for a listener of {@link Path} while {@link FileAccess#copy(Path, Path, FileCopyMode, PathCopyListener) copying}.
+ *
+ * @see FileAccess#copy(Path, Path, FileCopyMode, PathCopyListener)
+ */
+@FunctionalInterface
+public interface PathCopyListener {
+
+ /**
+ * An empty {@link PathCopyListener} instance doing nothing.
+ */
+ static PathCopyListener NONE = (s, t, d) -> {
+ };
+
+ /**
+ * @param source the {@link Path} of the source to copy.
+ * @param target the {@link Path} of the copied target.
+ * @param directory - {@code true} in case of {@link java.nio.file.Files#isDirectory(Path, java.nio.file.LinkOption...) directory}, {@code false}
+ * otherwise (regular file).
+ */
+ void onCopy(Path source, Path target, boolean directory);
+
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/AbstractIdeSubLogger.java b/cli/src/main/java/com/devonfw/tools/ide/log/AbstractIdeSubLogger.java
index d9050540d..0a4a1e873 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/log/AbstractIdeSubLogger.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/log/AbstractIdeSubLogger.java
@@ -8,15 +8,33 @@ public abstract class AbstractIdeSubLogger implements IdeSubLogger {
/** @see #getLevel() */
protected final IdeLogLevel level;
+ protected final IdeLogExceptionDetails exceptionDetails;
+
+ final IdeLogListener listener;
+
+ protected final boolean colored;
+
+ private boolean enabled;
+
+ private int count;
+
/**
* The constructor.
*
* @param level the {@link #getLevel() log-level}.
*/
- public AbstractIdeSubLogger(IdeLogLevel level) {
+ public AbstractIdeSubLogger(IdeLogLevel level, boolean colored, IdeLogExceptionDetails exceptionDetails, IdeLogListener listener) {
super();
this.level = level;
+ this.exceptionDetails = exceptionDetails;
+ if (listener == null) {
+ this.listener = IdeLogListenerNone.INSTANCE;
+ } else {
+ this.listener = listener;
+ }
+ this.colored = colored;
+ this.enabled = true;
}
@Override
@@ -25,6 +43,24 @@ public IdeLogLevel getLevel() {
return this.level;
}
+ @Override
+ public boolean isEnabled() {
+
+ return this.enabled;
+ }
+
+ void setEnabled(boolean enabled) {
+
+ this.enabled = enabled;
+ }
+
+
+ @Override
+ public int getCount() {
+
+ return this.count;
+ }
+
/**
* Should only be used internally by logger implementation.
*
@@ -87,11 +123,43 @@ private void warning(String message) {
/**
* @return {@code true} if colored logging is used, {@code false} otherwise.
*/
- protected boolean isColored() {
+ public boolean isColored() {
- return false;
+ return this.colored;
}
+ @Override
+ public String log(Throwable error, String message, Object... args) {
+
+ if (!this.enabled) {
+ this.count++;
+ // performance optimization: do not fill in arguments if disabled
+ return message;
+ }
+ String actualMessage = message;
+ if (message == null) {
+ if (error == null) {
+ actualMessage = "Internal error: Both message and error is null - nothing to log!";
+ // fail fast if assertions are enabled, so developers of IDEasy will find the bug immediately but in productive use better log the error and continue
+ assert false : actualMessage;
+ }
+ } else if (args != null) {
+ actualMessage = compose(actualMessage, args);
+ }
+ boolean accept = this.listener.onLog(this.level, actualMessage, message, args, error);
+ if (accept) {
+ this.count++;
+ doLog(actualMessage, error);
+ }
+ return actualMessage;
+ }
+
+ /**
+ * @param message the formatted message to log.
+ * @param error the optional {@link Throwable} to log or {@code null} for no error.
+ */
+ protected abstract void doLog(String message, Throwable error);
+
@Override
public String toString() {
diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java
new file mode 100644
index 000000000..f65e26e28
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java
@@ -0,0 +1,171 @@
+package com.devonfw.tools.ide.log;
+
+/**
+ * Single entry that was logged.
+ *
+ * @param level the {@link IdeLogLevel}.
+ * @param message the {@link IdeSubLogger#log(String) logged message}.
+ * @param rawMessage the {@link IdeSubLogger#log(String, Object...) raw message template}.
+ * @param args the {@link IdeSubLogger#log(String, Object...) optional message arguments}.
+ * @param error the {@link IdeSubLogger#log(Throwable, String) optional error that was logged}.
+ * @param contains - {@code true} if the {@link IdeLogEntry} to create is used as sub-string pattern for {@link #matches(IdeLogEntry) matching},
+ * {@code false} otherwise.
+ */
+public record IdeLogEntry(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error, boolean contains) {
+
+ /**
+ * The constructor.
+ *
+ * @param level - {@link #level()}.
+ * @param message - {@link #message()}.
+ * @param rawMessage - {@link #rawMessage()}.
+ * @param args - {@link #args()}.
+ * @param error - {@link #error()}.
+ * @param contains - {@link #contains()}.
+ */
+ public IdeLogEntry {
+
+ // validation to be used by all constructors...
+ if ((message == null) && (rawMessage == null)) {
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * @param level the {@link IdeLogLevel}.
+ * @param message the {@link IdeSubLogger#log(String) logged message}.
+ */
+ public IdeLogEntry(IdeLogLevel level, String message) {
+
+ this(level, message, null, null, null, false);
+ }
+
+ /**
+ * @param level the {@link IdeLogLevel}.
+ * @param message the {@link IdeSubLogger#log(String) logged message}.
+ * @param contains the {@link #contains()} flag.
+ */
+ public IdeLogEntry(IdeLogLevel level, String message, boolean contains) {
+
+ this(level, message, null, null, null, contains);
+ }
+
+ /**
+ * @param level the {@link IdeLogLevel}.
+ * @param message the {@link IdeSubLogger#log(String) logged message}.
+ * @param rawMessage the {@link IdeSubLogger#log(String, Object...) raw message template}.
+ * @param args the {@link IdeSubLogger#log(String, Object...) optional message arguments}.
+ */
+ public IdeLogEntry(IdeLogLevel level, String message, String rawMessage, Object[] args) {
+
+ this(level, message, rawMessage, args, null, false);
+ }
+
+ /**
+ * @param level the {@link IdeLogLevel}.
+ * @param message the {@link IdeSubLogger#log(String) logged message}.
+ * @param rawMessage the {@link IdeSubLogger#log(String, Object...) raw message template}.
+ * @param args the {@link IdeSubLogger#log(String, Object...) optional message arguments}.
+ * @param error the {@link IdeSubLogger#log(Throwable, String) optional error that was logged}.
+ */
+ public IdeLogEntry(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error) {
+
+ this(level, message, rawMessage, args, error, false);
+ }
+
+ /**
+ * ATTENTION: This method is not symmetric so it may be that x.matches(y) != y.matches(x). This method should always be called on the expected entry with the
+ * actually collected entry as parameter.
+ *
+ * @param entry the {@link IdeLogEntry} to match.
+ * @return {@code true} if the given {@link IdeLogEntry} matches to this one, {@code false} otherwise.
+ */
+ public boolean matches(IdeLogEntry entry) {
+
+ if (this.level != entry.level) {
+ return false;
+ } else if (this.contains) {
+ if (!entry.message.contains(this.message)) {
+ return false;
+ }
+ } else {
+ if (this.message != null && !entry.message.equals(this.message)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @param message the {@link #message() message}.
+ * @return the new {@link IdeLogEntry} with {@link IdeLogLevel#ERROR}.
+ */
+ public static IdeLogEntry ofError(String message) {
+
+ return new IdeLogEntry(IdeLogLevel.ERROR, message);
+ }
+
+ /**
+ * @param message the {@link #message() message}.
+ * @return the new {@link IdeLogEntry} with {@link IdeLogLevel#WARNING}.
+ */
+ public static IdeLogEntry ofWarning(String message) {
+
+ return new IdeLogEntry(IdeLogLevel.WARNING, message);
+ }
+
+ /**
+ * @param message the {@link #message() message}.
+ * @return the new {@link IdeLogEntry} with {@link IdeLogLevel#INFO}.
+ */
+ public static IdeLogEntry ofInfo(String message) {
+
+ return new IdeLogEntry(IdeLogLevel.INFO, message);
+ }
+
+ /**
+ * @param message the {@link #message() message}.
+ * @return the new {@link IdeLogEntry} with {@link IdeLogLevel#STEP}.
+ */
+ public static IdeLogEntry ofStep(String message) {
+
+ return new IdeLogEntry(IdeLogLevel.STEP, message);
+ }
+
+ /**
+ * @param message the {@link #message() message}.
+ * @return the new {@link IdeLogEntry} with {@link IdeLogLevel#SUCCESS}.
+ */
+ public static IdeLogEntry ofSuccess(String message) {
+
+ return new IdeLogEntry(IdeLogLevel.SUCCESS, message);
+ }
+
+ /**
+ * @param message the {@link #message() message}.
+ * @return the new {@link IdeLogEntry} with {@link IdeLogLevel#DEBUG}.
+ */
+ public static IdeLogEntry ofDebug(String message) {
+
+ return new IdeLogEntry(IdeLogLevel.DEBUG, message);
+ }
+
+ /**
+ * @param message the {@link #message() message}.
+ * @return the new {@link IdeLogEntry} with {@link IdeLogLevel#TRACE}.
+ */
+ public static IdeLogEntry ofTrace(String message) {
+
+ return new IdeLogEntry(IdeLogLevel.TRACE, message);
+ }
+
+ /**
+ * @param message the {@link #message() message}.
+ * @return the new {@link IdeLogEntry} with {@link IdeLogLevel#PROCESSABLE}.
+ */
+ public static IdeLogEntry ofProcessable(String message) {
+
+ return new IdeLogEntry(IdeLogLevel.PROCESSABLE, message);
+ }
+
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogExceptionDetails.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogExceptionDetails.java
index 50d7b8a00..7f0b38d0e 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogExceptionDetails.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogExceptionDetails.java
@@ -34,11 +34,24 @@ void format(Throwable error, StringWriter sw) {
void format(Throwable error, StringWriter sw) {
String errorMessage = error.getMessage();
- if (isBlank(errorMessage)) {
+ if ((errorMessage == null) || errorMessage.isBlank()) {
errorMessage = error.getClass().getName();
}
sw.append(errorMessage);
}
+ },
+
+ /** Ignore error and only log explicit message. */
+ NONE(0) {
+ @Override
+ String format(String message, Throwable error) {
+ return message;
+ }
+
+ @Override
+ void format(Throwable error, StringWriter sw) {
+
+ }
};
private final int capacityOffset;
@@ -54,20 +67,12 @@ private IdeLogExceptionDetails(int capacityOffset) {
*/
String format(String message, Throwable error) {
- boolean hasMessage = !isBlank(message);
- if (error == null) {
- if (hasMessage) {
- return message;
- } else {
- return "Internal error: Both message and error is null - nothing to log!";
- }
- }
int capacity = this.capacityOffset;
- if (hasMessage) {
+ if (message != null) {
capacity = capacity + message.length() + 1;
}
StringWriter sw = new StringWriter(capacity);
- if (hasMessage) {
+ if (message != null) {
sw.append(message);
sw.append('\n');
}
@@ -77,14 +82,6 @@ String format(String message, Throwable error) {
abstract void format(Throwable error, StringWriter sw);
- private static boolean isBlank(String string) {
-
- if ((string == null) || (string.isBlank())) {
- return true;
- }
- return false;
- }
-
/**
* @param level the {@link IdeLogLevel} of the {@link IdeSubLogger}.
* @param minLogLevel the minimum {@link IdeLogLevel} (threshold).
@@ -95,14 +92,11 @@ static IdeLogExceptionDetails of(IdeLogLevel level, IdeLogLevel minLogLevel) {
if ((minLogLevel == IdeLogLevel.TRACE) || (minLogLevel == IdeLogLevel.DEBUG)) {
return STACKTRACE;
}
- switch (level) {
- case ERROR:
- return STACKTRACE;
- case WARNING:
- return TO_STRING;
- default:
- return MESSAGE;
- }
+ return switch (level) {
+ case ERROR -> STACKTRACE;
+ case WARNING -> TO_STRING;
+ default -> MESSAGE;
+ };
}
}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogLevel.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogLevel.java
index 07ac4b30a..a6edec6b9 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogLevel.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogLevel.java
@@ -35,7 +35,10 @@ public enum IdeLogLevel {
/**
* {@link IdeLogLevel} for an error (something failed and we cannot proceed or the user has to continue with extreme care).
*/
- ERROR("\033[91m");
+ ERROR("\033[91m"),
+
+ /** {@link IdeLogLevel} for {@link com.devonfw.tools.ide.commandlet.Commandlet#isProcessableOutput() processable output} */
+ PROCESSABLE(null);
private final String color;
@@ -63,4 +66,12 @@ public String getEndColor() {
return "\033[0m"; // reset color
}
+ /**
+ * @return {@code true} in case of a custom log-level, {@code false} otherwise (standard log-level supported by SLF4J and all reasonable loggers).
+ */
+ public boolean isCustom() {
+
+ return (this == STEP) || (this == INTERACTION) || (this == SUCCESS) || (this == PROCESSABLE);
+ }
+
}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java
new file mode 100644
index 000000000..f768befe9
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java
@@ -0,0 +1,19 @@
+package com.devonfw.tools.ide.log;
+
+/**
+ * Interface to listen for log-events.
+ */
+@FunctionalInterface
+public interface IdeLogListener {
+
+ /**
+ * @param level the {@link IdeLogLevel}.
+ * @param message the actual message to be logged.
+ * @param rawMessage the raw message logged (without args filled in)
+ * @param args the optional message arguments.
+ * @param error the optional error.
+ * @return {@code true} to accept this log event (default), {@code false} otherwise to reject further processing and suppress the log message.
+ */
+ boolean onLog(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error);
+
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerBuffer.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerBuffer.java
new file mode 100644
index 000000000..b0bd8fba4
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerBuffer.java
@@ -0,0 +1,42 @@
+package com.devonfw.tools.ide.log;
+
+import java.util.List;
+
+/**
+ * Extends {@link IdeLogListenerCollector} to buffer log events during bootstrapping and then flush them once the logger is properly configured.
+ */
+public class IdeLogListenerBuffer extends IdeLogListenerCollector {
+
+ @Override
+ public boolean onLog(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error) {
+
+ if (this.entries == null) {
+ return true;
+ } else {
+ // buffer the log event
+ super.onLog(level, message, rawMessage, args, error);
+ // reject further processing of the log event suppressing it (so it is only cached)
+ return false;
+ }
+ }
+
+ /**
+ * This method is supposed to be called once after the {@link IdeLogger} has been properly initialized.
+ *
+ * @param logger the initialized {@link IdeLogger}.
+ */
+ public void flushAndDisable(IdeLogger logger) {
+
+ if (this.entries == null) {
+ return;
+ }
+ List buffer = this.entries;
+ // disable ourselves from collecting further events
+ this.entries = null;
+ // write all cached log events to the logger again for processing
+ for (IdeLogEntry entry : buffer) {
+ logger.level(entry.level()).log(entry.error(), entry.message());
+ }
+ }
+
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerCollector.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerCollector.java
new file mode 100644
index 000000000..32752f790
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerCollector.java
@@ -0,0 +1,36 @@
+package com.devonfw.tools.ide.log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implementation of {@link IdeLogListener} that collects all events as {@link IdeLogEntry}.
+ */
+public class IdeLogListenerCollector implements IdeLogListener {
+
+ protected List entries;
+
+ /**
+ * The constructor.
+ */
+ public IdeLogListenerCollector() {
+ super();
+ this.entries = new ArrayList<>(512);
+ }
+
+ /**
+ * @return the {@link List} of {@link IdeLogEntry} that have been logged for test assertions.
+ */
+ public List getEntries() {
+
+ return this.entries;
+ }
+
+ @Override
+ public boolean onLog(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error) {
+ if (this.entries != null) {
+ this.entries.add(new IdeLogEntry(level, message, rawMessage, args, error));
+ }
+ return true;
+ }
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerNone.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerNone.java
new file mode 100644
index 000000000..64ac76964
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerNone.java
@@ -0,0 +1,20 @@
+package com.devonfw.tools.ide.log;
+
+/**
+ * Implementation of {@link IdeLogListener} that does nothing.
+ */
+public class IdeLogListenerNone implements IdeLogListener {
+
+ /** The singleton instance. */
+ public static final IdeLogListenerNone INSTANCE = new IdeLogListenerNone();
+
+ private IdeLogListenerNone() {
+ super();
+ }
+
+ @Override
+ public boolean onLog(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error) {
+
+ return true;
+ }
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLoggerImpl.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLoggerImpl.java
new file mode 100644
index 000000000..d8f1a166b
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLoggerImpl.java
@@ -0,0 +1,85 @@
+package com.devonfw.tools.ide.log;
+
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * Implementation of {@link IdeLogger}.
+ */
+public class IdeLoggerImpl implements IdeLogger {
+
+ private final AbstractIdeSubLogger[] loggers;
+
+ protected final IdeLogListener listener;
+
+ /**
+ * @param minLogLevel the minimum enabled {@link IdeLogLevel}.
+ * @param factory the factory to create active {@link IdeSubLogger} instances.
+ */
+ public IdeLoggerImpl(IdeLogLevel minLogLevel, Function factory) {
+
+ super();
+ IdeLogLevel[] levels = IdeLogLevel.values();
+ this.loggers = new AbstractIdeSubLogger[levels.length];
+ IdeLogListener listener = null;
+ for (IdeLogLevel level : levels) {
+ this.loggers[level.ordinal()] = factory.apply(level);
+ if (listener == null) {
+ listener = this.loggers[level.ordinal()].listener;
+ }
+ }
+ this.listener = listener;
+ setLogLevel(minLogLevel);
+ }
+
+ @Override
+ public IdeSubLogger level(IdeLogLevel level) {
+
+ IdeSubLogger logger = this.loggers[level.ordinal()];
+ Objects.requireNonNull(logger);
+ return logger;
+ }
+
+ /**
+ * Sets the log level.
+ *
+ * @param logLevel {@link IdeLogLevel}
+ * @return the previous set logLevel {@link IdeLogLevel}
+ */
+ public IdeLogLevel setLogLevel(IdeLogLevel logLevel) {
+
+ IdeLogLevel previousLogLevel = null;
+ for (IdeLogLevel level : IdeLogLevel.values()) {
+ boolean enabled = level.ordinal() >= logLevel.ordinal();
+ if ((previousLogLevel == null) && this.loggers[level.ordinal()].isEnabled()) {
+ previousLogLevel = level;
+ }
+ setLogLevel(level, enabled);
+ }
+ if ((previousLogLevel == null) || (previousLogLevel.ordinal() > IdeLogLevel.INFO.ordinal())) {
+ previousLogLevel = IdeLogLevel.INFO;
+ }
+ return previousLogLevel;
+ }
+
+ /**
+ * @param logLevel the {@link IdeLogLevel} to modify.
+ * @param enabled - {@code true} to enable, {@code false} to disable.
+ */
+ public void setLogLevel(IdeLogLevel logLevel, boolean enabled) {
+
+ this.loggers[logLevel.ordinal()].setEnabled(enabled);
+ }
+
+ /**
+ * Ensure the logging system is initialized.
+ */
+ public void activateLogging() {
+
+ if (this.listener instanceof IdeLogListenerBuffer buffer) {
+ // https://github.com/devonfw/IDEasy/issues/754
+ buffer.flushAndDisable(this);
+ }
+ }
+
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLogger.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLogger.java
index a3caf808c..33adbce55 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLogger.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLogger.java
@@ -52,4 +52,12 @@ default String log(Throwable error, String message) {
*/
IdeLogLevel getLevel();
+ /**
+ * ATTENTION: When using static mock implementations via {@code IdeTestContextMock}, this value should never be asserted since it is accumulated over JVM
+ * lifetime.
+ *
+ * @return the number of {@code log} method calls that have been tracked by this {@link IdeSubLogger}.
+ */
+ int getCount();
+
}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLoggerNone.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLoggerNone.java
deleted file mode 100644
index 3b08c0c66..000000000
--- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLoggerNone.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package com.devonfw.tools.ide.log;
-
-/**
- * Implementation of {@link IdeSubLogger} that is NOT {@link #isEnabled() enabled} and does nothing.
- */
-public final class IdeSubLoggerNone extends AbstractIdeSubLogger {
-
- private static final IdeSubLoggerNone[] LOGGERS;
-
- static {
- IdeLogLevel[] levels = IdeLogLevel.values();
- LOGGERS = new IdeSubLoggerNone[levels.length];
- for (int i = 0; i < levels.length; i++) {
- LOGGERS[i] = new IdeSubLoggerNone(levels[i]);
- }
- }
-
- /**
- * The constructor.
- *
- * @param level the {@link #getLevel() log-level}.
- */
- private IdeSubLoggerNone(IdeLogLevel level) {
-
- super(level);
- }
-
- @Override
- public String log(Throwable error, String message, Object... args) {
-
- return message;
- }
-
- @Override
- public boolean isEnabled() {
-
- return false;
- }
-
- /**
- * @param level the {@link IdeLogLevel}.
- * @return the {@link IdeSubLoggerNone} instance.
- */
- public static IdeSubLoggerNone of(IdeLogLevel level) {
-
- return LOGGERS[level.ordinal()];
- }
-
-}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLoggerOut.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLoggerOut.java
index 902824895..acd79c75e 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLoggerOut.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeSubLoggerOut.java
@@ -10,10 +10,6 @@ public class IdeSubLoggerOut extends AbstractIdeSubLogger {
private final Appendable out;
- private final boolean colored;
-
- private final IdeLogExceptionDetails exceptionDetails;
-
/**
* The constructor.
*
@@ -22,9 +18,9 @@ public class IdeSubLoggerOut extends AbstractIdeSubLogger {
* @param colored - {@code true} for colored output according to {@link IdeLogLevel}, {@code false} otherwise.
* @param minLogLevel the minimum log level (threshold).
*/
- public IdeSubLoggerOut(IdeLogLevel level, Appendable out, boolean colored, IdeLogLevel minLogLevel) {
+ public IdeSubLoggerOut(IdeLogLevel level, Appendable out, boolean colored, IdeLogLevel minLogLevel, IdeLogListener listener) {
- super(level);
+ super(level, colored, IdeLogExceptionDetails.of(level, minLogLevel), listener);
if (out == null) {
// this is on of the very rare excuses where System.out or System.err is allowed to be used!
if (level == IdeLogLevel.ERROR) {
@@ -35,24 +31,10 @@ public IdeSubLoggerOut(IdeLogLevel level, Appendable out, boolean colored, IdeLo
} else {
this.out = out;
}
- this.colored = colored;
- this.exceptionDetails = IdeLogExceptionDetails.of(level, minLogLevel);
- }
-
- @Override
- public boolean isEnabled() {
-
- return true;
}
@Override
- protected boolean isColored() {
-
- return this.colored;
- }
-
- @Override
- public void log(String message) {
+ public void doLog(String message, Throwable error) {
try {
String startColor = null;
@@ -62,6 +44,9 @@ public void log(String message) {
this.out.append(startColor);
}
}
+ if (error != null) {
+ message = this.exceptionDetails.format(message, error);
+ }
this.out.append(message);
if (startColor != null) {
this.out.append(this.level.getEndColor());
@@ -71,23 +56,4 @@ public void log(String message) {
throw new IllegalStateException("Failed to log message: " + message, e);
}
}
-
- @Override
- public String log(Throwable error, String message, Object... args) {
-
- if (args != null) {
- message = compose(message, args);
- }
- log(this.exceptionDetails.format(message, error));
- if (message == null) {
- if (error == null) {
- return null;
- } else {
- return error.toString();
- }
- } else {
- return message;
- }
- }
-
}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/network/NetworkProxy.java b/cli/src/main/java/com/devonfw/tools/ide/network/NetworkProxy.java
new file mode 100644
index 000000000..80d78ada4
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/network/NetworkProxy.java
@@ -0,0 +1,126 @@
+package com.devonfw.tools.ide.network;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Locale;
+
+import com.devonfw.tools.ide.context.IdeContext;
+import com.devonfw.tools.ide.environment.IdeSystem;
+import com.devonfw.tools.ide.log.IdeLogLevel;
+
+/**
+ * Simple class to {@link #configure()} network proxy.
+ */
+public class NetworkProxy {
+
+ private static final String PROXY_DOCUMENTATION_PAGE = "https://github.com/devonfw/IDEasy/blob/main/documentation/proxy-support.adoc";
+
+ private final IdeContext context;
+
+ private String nonProxyHosts;
+
+ private String allProxy;
+
+ /**
+ * @param context the {@link IdeContext}.
+ */
+ public NetworkProxy(IdeContext context) {
+
+ super();
+ this.context = context;
+ }
+
+ /**
+ * Perform the actual {@link NetworkProxy} configuration.
+ */
+ public void configure() {
+
+ setupNetworkProxy("http");
+ setupNetworkProxy("https");
+ }
+
+ private void setupNetworkProxy(String protocol) {
+
+ String systemPropertyProxyHost = protocol + ".proxyHost";
+ String configuredValue = System.getProperty(systemPropertyProxyHost);
+ if (configuredValue != null) {
+ this.context.trace("Proxy already configured via system property {}={}", systemPropertyProxyHost, configuredValue);
+ return;
+ }
+ String proxyUrlString = getProxyUrlFromEnvironmentVariable(protocol);
+ if (proxyUrlString == null) {
+ this.context.trace("No {} proxy configured.", protocol);
+ return;
+ }
+ try {
+ URL proxyUrl = new URL(proxyUrlString);
+ IdeSystem system = this.context.getSystem();
+ system.setProperty(systemPropertyProxyHost, proxyUrl.getHost());
+ int port = proxyUrl.getPort();
+ if (port == -1) {
+ String urlProtocol = proxyUrl.getProtocol().toLowerCase(Locale.ROOT);
+ if ("http".equals(urlProtocol)) {
+ port = 80;
+ } else if ("https".equals(urlProtocol)) {
+ port = 443;
+ } else if ("ftp".equals(urlProtocol)) {
+ port = 21;
+ }
+ }
+ system.setProperty(protocol + ".proxyPort", Integer.toString(port));
+ if (this.nonProxyHosts == null) {
+ this.nonProxyHosts = getEnvironmentVariableNonNull("no_proxy");
+ }
+ if (!this.nonProxyHosts.isEmpty()) {
+ system.setProperty(protocol + ".nonProxyHosts", this.nonProxyHosts);
+ }
+ } catch (MalformedURLException e) {
+ context.level(IdeLogLevel.WARNING)
+ .log(e, "Invalid {} proxy configuration detected with URL {}. Proxy configuration will be skipped.\n"
+ + "For further details, see " + PROXY_DOCUMENTATION_PAGE, protocol, proxyUrlString);
+ }
+ }
+
+ private String getProxyUrlFromEnvironmentVariable(String protocol) {
+
+ String proxyUrl = getEnvironmentVariableCaseInsensitive(protocol + "_proxy");
+ if (proxyUrl == null) {
+ if (this.allProxy == null) {
+ this.allProxy = getEnvironmentVariableNonNull("all_proxy");
+ }
+ if (!this.allProxy.isEmpty()) {
+ proxyUrl = this.allProxy;
+ }
+ }
+ return proxyUrl;
+ }
+
+ private String getEnvironmentVariableNonNull(String nameLowerCase) {
+
+ String value = getEnvironmentVariableCaseInsensitive(nameLowerCase);
+ if (value == null) {
+ return "";
+ } else {
+ return value.trim();
+ }
+ }
+
+ private String getEnvironmentVariableCaseInsensitive(String nameLowerCase) {
+
+ String value = getEnvironmentVariable(nameLowerCase);
+ if (value == null) {
+ value = getEnvironmentVariable(nameLowerCase.toUpperCase(Locale.ROOT));
+ }
+ return value;
+ }
+
+ private String getEnvironmentVariable(String name) {
+
+ String value = this.context.getSystem().getEnv(name);
+ if (value != null) {
+ this.context.trace("Found environment variable {}={}", name, value);
+ }
+ return value;
+ }
+
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/network/ProxyConfig.java b/cli/src/main/java/com/devonfw/tools/ide/network/ProxyConfig.java
deleted file mode 100644
index 347750838..000000000
--- a/cli/src/main/java/com/devonfw/tools/ide/network/ProxyConfig.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.devonfw.tools.ide.network;
-
-import java.net.MalformedURLException;
-import java.net.URL;
-
-import com.devonfw.tools.ide.context.IdeContext;
-
-/**
- * Class responsible for parsing and storing the host and port information from a given proxy URL.
- */
-public class ProxyConfig {
-
- private final IdeContext context;
-
- private String host;
-
- private int port;
-
- ProxyConfig(String proxyUrl, IdeContext context) {
-
- this.context = context;
-
- try {
- URL url = new URL(proxyUrl);
- this.host = url.getHost();
- this.port = url.getPort();
- } catch (MalformedURLException e) {
- this.context.warning(ProxyContext.PROXY_FORMAT_WARNING_MESSAGE);
- }
- }
-
- /**
- * @return a {@link String} representing the host of the proxy
- */
- public String getHost() {
-
- return this.host;
- }
-
- /**
- * @return an {@code int} representing the port of the proxy
- */
- public int getPort() {
-
- return this.port;
- }
-}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/network/ProxyContext.java b/cli/src/main/java/com/devonfw/tools/ide/network/ProxyContext.java
deleted file mode 100644
index c53af016d..000000000
--- a/cli/src/main/java/com/devonfw/tools/ide/network/ProxyContext.java
+++ /dev/null
@@ -1,96 +0,0 @@
-package com.devonfw.tools.ide.network;
-
-import java.net.InetSocketAddress;
-import java.net.Proxy;
-
-import com.devonfw.tools.ide.context.IdeContext;
-
-/**
- * Class for handling system proxy settings. This class is responsible for detecting and managing the proxy configurations for HTTP and HTTPS protocols based on
- * the system's environment variables.
- */
-public class ProxyContext {
-
- private final IdeContext context;
-
- private static final String HTTP_PROXY = "http_proxy";
-
- private static final String HTTPS_PROXY = "https_proxy";
-
- private static final String PROXY_DOCUMENTATION_PAGE = "https://github.com/devonfw/IDEasy/blob/main/documentation/proxy-support.adoc";
-
- static final String PROXY_FORMAT_WARNING_MESSAGE =
- "Proxy configuration detected, but the formatting appears to be incorrect. Proxy configuration will be skipped.\n"
- + "Please note that IDEasy can detect a proxy only if the corresponding environmental variables are properly formatted. "
- + "For further details, see " + PROXY_DOCUMENTATION_PAGE;
-
- final private ProxyConfig httpProxyConfig;
-
- final private ProxyConfig httpsProxyConfig;
-
- /**
- * Class to detect system proxy configurations
- *
- * @param context the {@link IdeContext}
- */
- public ProxyContext(IdeContext context) {
-
- this.context = context;
- this.httpProxyConfig = initializeProxyConfig(HTTP_PROXY);
- this.httpsProxyConfig = initializeProxyConfig(HTTPS_PROXY);
- }
-
- private ProxyConfig initializeProxyConfig(String proxyEnvVariable) {
-
- String proxyUrl = System.getenv(proxyEnvVariable);
- if (proxyUrl == null) {
- proxyUrl = System.getenv(proxyEnvVariable.toUpperCase());
- }
- return (proxyUrl != null && !proxyUrl.isEmpty()) ? new ProxyConfig(proxyUrl, this.context) : null;
- }
-
- /**
- * Retrieves the system proxy for a given URL.
- *
- * @param url The URL of the request for which to detect a proxy. This is used to determine the corresponding proxy based on the protocol.
- * @return A {@link Proxy} object representing the system proxy for the given URL, or {@link Proxy#NO_PROXY} if no valid proxy is found or if the proxy
- * configuration is invalid.
- */
- public Proxy getProxy(String url) {
-
- ProxyConfig proxyConfig = getProxyConfig(url);
- if (proxyConfig != null) {
- String proxyHost = proxyConfig.getHost();
- int proxyPort = proxyConfig.getPort();
-
- if (proxyHost != null && !proxyHost.isEmpty() && proxyPort > 0 && proxyPort <= 65535) {
- InetSocketAddress proxyAddress = new InetSocketAddress(proxyHost, proxyPort);
- if (proxyAddress.isUnresolved()) {
- this.context.warning(ProxyContext.PROXY_FORMAT_WARNING_MESSAGE);
- return Proxy.NO_PROXY;
- }
- return new Proxy(Proxy.Type.HTTP, proxyAddress);
- }
- }
- return Proxy.NO_PROXY;
- }
-
- /**
- * Retrieves the appropriate {@link ProxyConfig} object based on the given request URL.
- *
- * @param url a {@link String} representing the URL for which the related proxy is to be determined
- * @return a {@link ProxyConfig} object with the correct settings, or {@code null} if the URL is malformed
- */
- public ProxyConfig getProxyConfig(String url) {
-
- if (url.startsWith("http://")) {
- return this.httpProxyConfig;
- } else if (url.startsWith("https://")) {
- return this.httpsProxyConfig;
- } else {
- this.context.warning("Download URL wrongly formatted: " + url);
- return null;
- }
- }
-}
-
diff --git a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java
index ed6a7c90f..488491ce0 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java
@@ -152,6 +152,7 @@ public ProcessResult run(ProcessMode processMode) {
List args = new ArrayList<>(this.arguments.size() + 4);
String interpreter = addExecutable(this.executable.toString(), args);
args.addAll(this.arguments);
+ String command = createCommand();
if (this.context.debug().isEnabled()) {
String message = createCommandMessage(interpreter, " ...");
this.context.debug(message);
@@ -173,26 +174,32 @@ public ProcessResult run(ProcessMode processMode) {
Process process = this.processBuilder.start();
- if (processMode == ProcessMode.DEFAULT_CAPTURE) {
- CompletableFuture> outFut = readInputStream(process.getInputStream(), false, logs);
- CompletableFuture> errFut = readInputStream(process.getErrorStream(), true, logs);
- out = outFut.get();
- err = errFut.get();
- }
+ try {
+ if (processMode == ProcessMode.DEFAULT_CAPTURE) {
+ CompletableFuture> outFut = readInputStream(process.getInputStream());
+ CompletableFuture> errFut = readInputStream(process.getErrorStream());
+ out = outFut.get();
+ err = errFut.get();
+ }
- int exitCode;
+ int exitCode;
- if (processMode.isBackground()) {
- exitCode = ProcessResult.SUCCESS;
- } else {
- exitCode = process.waitFor();
- }
+ if (processMode.isBackground()) {
+ exitCode = ProcessResult.SUCCESS;
+ } else {
+ exitCode = process.waitFor();
+ }
- ProcessResult result = new ProcessResultImpl(exitCode, out, err, logs);
+ ProcessResult result = new ProcessResultImpl(this.executable.getFileName().toString(), command, exitCode, out, err);
- performLogging(result, exitCode, interpreter);
- return result;
+ performLogging(result, exitCode, interpreter);
+ return result;
+ } finally {
+ if (!processMode.isBackground()) {
+ process.destroy();
+ }
+ }
} catch (CliProcessException | IllegalStateException e) {
// these exceptions are thrown from performLogOnError and we do not want to wrap them (see #593)
throw e;
@@ -234,6 +241,17 @@ private static CompletableFuture> readInputStream(InputStream is, b
});
}
+ private String createCommand() {
+ String cmd = this.executable.toString();
+ StringBuilder sb = new StringBuilder(cmd.length() + this.arguments.size() * 4);
+ sb.append(cmd);
+ for (String arg : this.arguments) {
+ sb.append(' ');
+ sb.append(arg);
+ }
+ return sb.toString();
+ }
+
private String createCommandMessage(String interpreter, String suffix) {
StringBuilder sb = new StringBuilder();
diff --git a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessResult.java b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessResult.java
index f8c4cb8aa..85a6d6bbb 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessResult.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessResult.java
@@ -2,6 +2,7 @@
import java.util.List;
+import com.devonfw.tools.ide.cli.CliProcessException;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.log.IdeLogLevel;
@@ -24,6 +25,9 @@ public interface ProcessResult {
/** Return code if tool was requested that is not installed. */
int TOOL_NOT_INSTALLED = 4;
+ /** Return code to exit if condition not met */
+ int EXIT = 17;
+
/**
* Return code to abort gracefully.
*
@@ -38,6 +42,17 @@ public interface ProcessResult {
*/
int OFFLINE = 23;
+ /**
+ * @return the filename of the executable that was run (e.g. "git").
+ * @see #getCommand()
+ */
+ String getExecutable();
+
+ /**
+ * @return the full command that was executed (e.g. "git rev-parse HEAD").
+ */
+ String getCommand();
+
/**
* @return the exit code. Will be {@link #SUCCESS} on successful completion of the {@link Process}.
*/
@@ -77,4 +92,11 @@ default boolean isSuccessful() {
* @param errorLevel the {@link IdeLogLevel} to use for {@link #getErr()}.
*/
void log(IdeLogLevel outLevel, IdeContext context, IdeLogLevel errorLevel);
+
+ /**
+ * Throws a {@link CliProcessException} if not {@link #isSuccessful() successful} and otherwise does nothing.
+ *
+ * @throws CliProcessException if not {@link #isSuccessful() successful}.
+ */
+ void failOnError() throws CliProcessException;
}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessResultImpl.java b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessResultImpl.java
index 980ce6ae2..84420d2be 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessResultImpl.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessResultImpl.java
@@ -4,6 +4,7 @@
import java.util.List;
import java.util.Objects;
+import com.devonfw.tools.ide.cli.CliProcessException;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.log.IdeLogLevel;
@@ -12,6 +13,10 @@
*/
public class ProcessResultImpl implements ProcessResult {
+ private final String executable;
+
+ private final String command;
+
private final int exitCode;
private final List out;
@@ -23,20 +28,36 @@ public class ProcessResultImpl implements ProcessResult {
/**
* The constructor.
*
+ * @param executable the {@link #getExecutable() executable}.
+ * @param command the {@link #getCommand() command}.
* @param exitCode the {@link #getExitCode() exit code}.
* @param out the {@link #getOut() out}.
* @param err the {@link #getErr() err}.
* @param logEvents the {@link #getLogEvents()} () logEvents}.
*/
- public ProcessResultImpl(int exitCode, List out, List err, List logEvents) {
+ public ProcessResultImpl(String executable, String command, int exitCode, List out, List err) {
super();
+ this.executable = executable;
+ this.command = command;
this.exitCode = exitCode;
this.out = Objects.requireNonNullElse(out, Collections.emptyList());
this.err = Objects.requireNonNullElse(err, Collections.emptyList());
this.logEvents = Objects.requireNonNullElse(logEvents, Collections.emptyList());
}
+ @Override
+ public String getExecutable() {
+
+ return this.executable;
+ }
+
+ @Override
+ public String getCommand() {
+
+ return this.command;
+ }
+
@Override
public int getExitCode() {
@@ -65,6 +86,7 @@ public void log(IdeLogLevel level, IdeContext context) {
log(level, context, level);
}
+ @Override
public void log(IdeLogLevel outLevel, IdeContext context, IdeLogLevel errorLevel) {
if (!this.out.isEmpty()) {
@@ -85,4 +107,11 @@ private void doLog(IdeLogLevel level, List lines, IdeContext context) {
}
}
+ @Override
+ public void failOnError() throws CliProcessException {
+
+ if (!isSuccessful()) {
+ throw new CliProcessException(this);
+ }
+ }
}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/KeywordProperty.java b/cli/src/main/java/com/devonfw/tools/ide/property/KeywordProperty.java
index f3cf0a3a1..fd615e876 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/property/KeywordProperty.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/property/KeywordProperty.java
@@ -10,6 +10,8 @@
*/
public class KeywordProperty extends BooleanProperty {
+ private final String optionName;
+
/**
* The constructor.
*
@@ -19,8 +21,23 @@ public class KeywordProperty extends BooleanProperty {
*/
public KeywordProperty(String name, boolean required, String alias) {
- super(name, required, alias);
- assert (!name.isEmpty() && isValue());
+ super(getNormalizedName(name), required, alias);
+ this.optionName = name;
+ }
+
+ private static String getNormalizedName(String name) {
+
+ assert !name.isEmpty();
+ if (name.startsWith("--")) {
+ return name.substring(2);
+ }
+ return name;
+ }
+
+ @Override
+ public boolean isValue() {
+
+ return true;
}
@Override
@@ -29,6 +46,25 @@ public boolean isExpectValue() {
return false;
}
+ @Override
+ public boolean matches(String nameOrAlias) {
+
+ if (super.matches(nameOrAlias)) {
+ return true;
+ }
+ return this.optionName.equals(nameOrAlias);
+ }
+
+ @Override
+ public boolean apply(CliArguments args, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
+
+ String normalizedName = this.name;
+ if (args.current().isOption()) {
+ normalizedName = this.optionName;
+ }
+ return apply(normalizedName, args, context, commandlet, collector);
+ }
+
@Override
protected boolean applyValue(String argValue, boolean lookahead, CliArguments args, IdeContext context, Commandlet commandlet,
CompletionCandidateCollector collector) {
diff --git a/cli/src/main/java/com/devonfw/tools/ide/property/Property.java b/cli/src/main/java/com/devonfw/tools/ide/property/Property.java
index b96effa7b..0bf5ccc3f 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/property/Property.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/property/Property.java
@@ -85,7 +85,7 @@ public Property(String name, boolean required, String alias, boolean multivalued
}
/**
- * @return the name of this property.
+ * @return the name of this property. Will be the empty {@link String} for a {@link #isValue() value} property that is not a keyword.
*/
public String getName() {
@@ -253,6 +253,10 @@ public void clearValue() {
this.value.clear();
}
+ /**
+ * @param value the value to add to the {@link List} of values.
+ * @see #isMultiValued()
+ */
public void addValue(V value) {
if (!this.multivalued) {
@@ -331,21 +335,35 @@ protected V getNullValue() {
*/
public boolean apply(CliArguments args, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
+ return apply(this.name, args, context, commandlet, collector);
+ }
+
+ /**
+ * @param normalizedName the {@link #getName() name} or potentially a normalized form of it (see {@link KeywordProperty}).
+ * @param args the {@link CliArguments} already {@link CliArguments#current() pointing} the {@link CliArgument} to apply.
+ * @param context the {@link IdeContext}.
+ * @param commandlet the {@link Commandlet} owning this property.
+ * @param collector the {@link CompletionCandidateCollector}.
+ * @return {@code true} if it matches, {@code false} otherwise.
+ */
+ protected boolean apply(String normalizedName, CliArguments args, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
+
CliArgument argument = args.current();
if (argument.isCompletion()) {
int size = collector.getCandidates().size();
- complete(argument, args, context, commandlet, collector);
- if (collector.getCandidates().size() > size) { // completions added so complete matched?
- return true;
- }
+ complete(normalizedName, argument, args, context, commandlet, collector);
+ return (collector.getCandidates().size() > size);
}
- boolean option = isOption();
+ boolean option = normalizedName.startsWith("-");
if (option && !argument.isOption()) {
return false;
}
+ if (!option && argument.isOption() && (argument.get().length() > 1) && args.isSplitShortOpts()) {
+ return false;
+ }
String argValue = null;
boolean lookahead = false;
- if (this.name.isEmpty()) {
+ if (normalizedName.isEmpty()) {
argValue = argument.get();
} else {
if (!matches(argument.getKey())) {
@@ -384,7 +402,6 @@ protected boolean applyValue(String argValue, boolean lookahead, CliArguments ar
if (success) {
if (this.multivalued) {
-
while (success && args.hasNext()) {
CliArgument arg = args.next();
success = assignValueAsString(arg.get(), context, commandlet);
@@ -398,16 +415,18 @@ protected boolean applyValue(String argValue, boolean lookahead, CliArguments ar
/**
* Performs auto-completion for the {@code arg}.
*
+ * @param normalizedName the {@link #getName() name} or potentially a normalized form of it (see {@link KeywordProperty}).
* @param argument the {@link CliArgument CLI argument}.
* @param args the {@link CliArguments}.
* @param context the {@link IdeContext}.
* @param commandlet the {@link Commandlet} owning this {@link Property}.
* @param collector the {@link CompletionCandidateCollector}.
*/
- protected void complete(CliArgument argument, CliArguments args, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {
+ protected void complete(String normalizedName, CliArgument argument, CliArguments args, IdeContext context, Commandlet commandlet,
+ CompletionCandidateCollector collector) {
String arg = argument.get();
- if (this.name.isEmpty()) {
+ if (normalizedName.isEmpty()) {
int count = collector.getCandidates().size();
completeValue(arg, context, commandlet, collector);
if (collector.getCandidates().size() > count) {
@@ -415,8 +434,8 @@ protected void complete(CliArgument argument, CliArguments args, IdeContext cont
}
return;
}
- if (this.name.startsWith(arg)) {
- collector.add(this.name, null, this, commandlet);
+ if (normalizedName.startsWith(arg)) {
+ collector.add(normalizedName, null, this, commandlet);
}
if (this.alias != null) {
if (this.alias.startsWith(arg)) {
@@ -431,7 +450,7 @@ protected void complete(CliArgument argument, CliArguments args, IdeContext cont
String value = argument.getValue();
if (value != null) {
String key = argument.getKey();
- if (this.name.equals(key) || Objects.equals(this.alias, key)) {
+ if (normalizedName.equals(key) || Objects.equals(this.alias, key)) {
completeValue(value, context, commandlet, new CompletionCandidateCollectorAdapter(key + "=", collector));
}
}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/step/StepImpl.java b/cli/src/main/java/com/devonfw/tools/ide/step/StepImpl.java
index 7788e0068..cfce221e3 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/step/StepImpl.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/step/StepImpl.java
@@ -169,6 +169,9 @@ private void end(Boolean newSuccess, Throwable error, boolean suppress, String m
this.errorMessage = message;
}
} else {
+ if (message == null) {
+ message = error.toString();
+ }
this.errorMessage = this.context.error().log(error, message, args);
}
logger = this.context.debug();
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/DelegatingToolCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/DelegatingToolCommandlet.java
new file mode 100644
index 000000000..ffe188212
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/DelegatingToolCommandlet.java
@@ -0,0 +1,75 @@
+package com.devonfw.tools.ide.tool;
+
+import java.util.Set;
+
+import com.devonfw.tools.ide.common.Tag;
+import com.devonfw.tools.ide.context.IdeContext;
+import com.devonfw.tools.ide.environment.EnvironmentVariablesFiles;
+import com.devonfw.tools.ide.process.EnvironmentContext;
+import com.devonfw.tools.ide.version.VersionIdentifier;
+
+/**
+ * {@link ToolCommandlet} that delegates to another ToolCommandlet.
+ */
+public abstract class DelegatingToolCommandlet extends ToolCommandlet {
+
+ private Class delegateClass;
+
+ /**
+ * The constructor.
+ *
+ * @param context the {@link IdeContext}.
+ * @param tool the {@link #getName() tool name}.
+ * @param tags the {@link #getTags() tags} classifying the tool. Should be created via {@link Set#of(Object) Set.of} method.
+ * @param delegateClass the {@link ToolCommandlet}.
+ */
+ public DelegatingToolCommandlet(IdeContext context, String tool, Set tags, Class delegateClass) {
+
+ super(context, tool, tags);
+ this.delegateClass = delegateClass;
+ }
+
+ private D getDelegate() {
+ return getCommandlet(this.delegateClass);
+ }
+
+ @Override
+ public final boolean install(boolean silent, EnvironmentContext environmentContext) {
+ return getDelegate().install(silent, environmentContext);
+ }
+
+ @Override
+ public VersionIdentifier getInstalledVersion() {
+ return getDelegate().getInstalledVersion();
+ }
+
+ @Override
+ public String getInstalledEdition() {
+ return getDelegate().getInstalledEdition();
+ }
+
+ @Override
+ public void uninstall() {
+ getDelegate().uninstall();
+ }
+
+ @Override
+ public void listEditions() {
+ getDelegate().listEditions();
+ }
+
+ @Override
+ public void listVersions() {
+ getDelegate().listVersions();
+ }
+
+ @Override
+ public void setVersion(VersionIdentifier version, boolean hint, EnvironmentVariablesFiles destination) {
+ getDelegate().setVersion(version, hint, destination);
+ }
+
+ @Override
+ public void setEdition(String edition, boolean hint, EnvironmentVariablesFiles destination) {
+ getDelegate().setEdition(edition, hint, destination);
+ }
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java
index 08614fc7b..53f04d5c0 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java
@@ -67,7 +67,7 @@ protected void installDependencies() {
}
@Override
- public final boolean install(boolean silent, EnvironmentContext environmentContext) {
+ public boolean install(boolean silent, EnvironmentContext environmentContext) {
installDependencies();
VersionIdentifier configuredVersion = getConfiguredVersion();
@@ -75,18 +75,13 @@ public final boolean install(boolean silent, EnvironmentContext environmentConte
VersionIdentifier installedVersion = getInstalledVersion();
Step step = this.context.newStep(silent, "Install " + this.tool, configuredVersion);
try {
- // TODO https://github.com/devonfw/IDEasy/issues/664
- boolean enableOptimization = false;
- // performance: avoid calling installTool if already up-to-date
- if (enableOptimization & configuredVersion.equals(installedVersion)) { // here we can add https://github.com/devonfw/IDEasy/issues/637
- return toolAlreadyInstalled(silent, installedVersion, step);
- }
// install configured version of our tool in the software repository if not already installed
ToolInstallation installation = installTool(configuredVersion, environmentContext);
// check if we already have this version installed (linked) locally in IDE_HOME/software
VersionIdentifier resolvedVersion = installation.resolvedVersion();
- if (resolvedVersion.equals(installedVersion) && !installation.newInstallation()) {
+ if ((resolvedVersion.equals(installedVersion) && !installation.newInstallation())
+ || (configuredVersion.matches(installedVersion) && context.isSkipUpdatesMode())) {
return toolAlreadyInstalled(silent, installedVersion, step);
}
if (!isIgnoreSoftwareRepo()) {
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/androidstudio/AndroidStudio.java b/cli/src/main/java/com/devonfw/tools/ide/tool/androidstudio/AndroidStudio.java
index 5c81aeb3c..622ae6fcc 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/tool/androidstudio/AndroidStudio.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/androidstudio/AndroidStudio.java
@@ -1,6 +1,6 @@
package com.devonfw.tools.ide.tool.androidstudio;
-import java.nio.file.Path;
+import java.nio.file.Files;
import java.util.Set;
import com.devonfw.tools.ide.common.Tag;
@@ -15,6 +15,12 @@
*/
public class AndroidStudio extends IdeaBasedIdeToolCommandlet {
+ private static final String STUDIO = "studio";
+
+ private static final String STUDIO64_EXE = STUDIO + "64.exe";
+
+ private static final String STUDIO_BASH_SCRIPT = STUDIO + ".sh";
+
/**
* The constructor.
*
@@ -26,24 +32,25 @@ public AndroidStudio(IdeContext context) {
}
@Override
- protected void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean extraInstallation) {
+ protected String getBinaryName() {
- super.setEnvironment(environmentContext, toolInstallation, extraInstallation);
- environmentContext.withEnvVar("STUDIO_PROPERTIES", this.context.getWorkspacePath().resolve("studio.properties").toString());
+ if (this.context.getSystemInfo().isWindows()) {
+ return STUDIO64_EXE;
+ } else {
+ if (Files.exists(this.getToolBinPath().resolve(STUDIO))) {
+ return STUDIO;
+ } else if (Files.exists(this.getToolBinPath().resolve(STUDIO_BASH_SCRIPT))) {
+ return STUDIO_BASH_SCRIPT;
+ } else {
+ return STUDIO;
+ }
+ }
}
@Override
- protected void postExtract(Path extractedDir) {
+ protected void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean extraInstallation) {
- super.postExtract(extractedDir);
- String binaryName;
- if (this.context.getSystemInfo().isWindows()) {
- binaryName = "studio64.exe";
- } else if (this.context.getSystemInfo().isMac()) {
- binaryName = "studio";
- } else {
- binaryName = "studio.sh";
- }
- createStartScript(extractedDir, binaryName, true);
+ super.setEnvironment(environmentContext, toolInstallation, extraInstallation);
+ environmentContext.withEnvVar("STUDIO_PROPERTIES", this.context.getWorkspacePath().resolve("studio.properties").toString());
}
}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/ide/IdeToolCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/ide/IdeToolCommandlet.java
index 4bba7352f..c1f265337 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/tool/ide/IdeToolCommandlet.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/ide/IdeToolCommandlet.java
@@ -35,11 +35,11 @@ public IdeToolCommandlet(IdeContext context, String tool, Set tags) {
private boolean hasIde(Set tags) {
for (Tag tag : tags) {
- if (tag.isAncestorOf(Tag.IDE)) {
+ if (tag.isAncestorOf(Tag.IDE) || (tag == Tag.IDE)) {
return true;
}
}
- throw new IllegalStateException("Tags of IdeTool hat to be connected with tag IDE: " + tags);
+ throw new IllegalStateException("Tags of IdeTool has to be connected with tag IDE: " + tags);
}
@Override
@@ -62,7 +62,7 @@ protected void configureWorkspace() {
Path settingsWorkspaceFolder = this.context.getSettingsPath().resolve(this.tool)
.resolve(IdeContext.FOLDER_WORKSPACE);
- Path genericWorkspaceFolder = this.context.getSettingsPath().resolve(IdeContext.FOLDER_WORKSPACE);
+ Path genericWorkspaceFolder = this.context.getSettingsPath().resolve(IdeContext.FOLDER_WORKSPACE);
Path workspaceUpdateFolder = genericWorkspaceFolder.resolve(IdeContext.FOLDER_UPDATE);
Path workspaceSetupFolder = genericWorkspaceFolder.resolve(IdeContext.FOLDER_SETUP);
FileAccess fileAccess = this.context.getFileAccess();
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/intellij/Intellij.java b/cli/src/main/java/com/devonfw/tools/ide/tool/intellij/Intellij.java
index 14504869b..e0743d060 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/tool/intellij/Intellij.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/intellij/Intellij.java
@@ -1,6 +1,6 @@
package com.devonfw.tools.ide.tool.intellij;
-import java.nio.file.Path;
+import java.nio.file.Files;
import java.util.Set;
import com.devonfw.tools.ide.common.Tag;
@@ -32,6 +32,22 @@ public Intellij(IdeContext context) {
super(context, "intellij", Set.of(Tag.INTELLIJ));
}
+ @Override
+ protected String getBinaryName() {
+
+ if (this.context.getSystemInfo().isWindows()) {
+ return IDEA64_EXE;
+ } else {
+ if (Files.exists(this.getToolBinPath().resolve(IDEA))) {
+ return IDEA;
+ } else if (Files.exists(this.getToolBinPath().resolve(IDEA_BASH_SCRIPT))) {
+ return IDEA_BASH_SCRIPT;
+ } else {
+ return IDEA;
+ }
+ }
+ }
+
@Override
protected void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean extraInstallation) {
@@ -47,19 +63,4 @@ protected void installDependencies() {
getCommandlet(Java.class).install();
}
- @Override
- protected void postExtract(Path extractedDir) {
-
- super.postExtract(extractedDir);
- String binaryName;
- if (this.context.getSystemInfo().isWindows()) {
- binaryName = IDEA64_EXE;
- } else if (this.context.getSystemInfo().isMac()) {
- binaryName = IDEA;
- } else {
- binaryName = IDEA_BASH_SCRIPT;
- }
- createStartScript(extractedDir, binaryName, true);
- }
-
}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/kubectl/KubeCtl.java b/cli/src/main/java/com/devonfw/tools/ide/tool/kubectl/KubeCtl.java
new file mode 100644
index 000000000..c976413ee
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/kubectl/KubeCtl.java
@@ -0,0 +1,25 @@
+package com.devonfw.tools.ide.tool.kubectl;
+
+import java.util.Set;
+
+import com.devonfw.tools.ide.common.Tag;
+import com.devonfw.tools.ide.context.IdeContext;
+import com.devonfw.tools.ide.tool.DelegatingToolCommandlet;
+import com.devonfw.tools.ide.tool.docker.Docker;
+
+/**
+ * {@link DelegatingToolCommandlet} for Kubectl.
+ */
+public class KubeCtl extends DelegatingToolCommandlet {
+
+
+ /**
+ * The constructor.
+ *
+ * @param context the {@link IdeContext}.
+ */
+ public KubeCtl(IdeContext context) {
+
+ super(context, "kubectl", Set.of(Tag.KUBERNETES), Docker.class);
+ }
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/mvn/Mvn.java b/cli/src/main/java/com/devonfw/tools/ide/tool/mvn/Mvn.java
index 7bb762f00..b113e20a7 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/tool/mvn/Mvn.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/mvn/Mvn.java
@@ -10,7 +10,7 @@
import java.util.regex.Matcher;
import com.devonfw.tools.ide.common.Tag;
-import com.devonfw.tools.ide.context.GitContext;
+import com.devonfw.tools.ide.git.GitContext;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.process.ProcessContext;
import com.devonfw.tools.ide.process.ProcessMode;
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/tomcat/Tomcat.java b/cli/src/main/java/com/devonfw/tools/ide/tool/tomcat/Tomcat.java
index 109e10634..0f7c8b7e3 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/tool/tomcat/Tomcat.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/tomcat/Tomcat.java
@@ -29,6 +29,10 @@
*/
public class Tomcat extends LocalToolCommandlet {
+ private static final String CATALINA = "catalina";
+ private static final String CATALINA_BAT = CATALINA + ".bat";
+ private static final String CATALINA_BASH_SCRIPT = CATALINA + ".sh";
+
/**
* The constructor.
*
@@ -39,6 +43,16 @@ public Tomcat(IdeContext context) {
super(context, "tomcat", Set.of(Tag.JAVA));
}
+ @Override
+ protected String getBinaryName() {
+
+ if (this.context.getSystemInfo().isWindows()) {
+ return CATALINA_BAT;
+ } else {
+ return CATALINA_BASH_SCRIPT;
+ }
+ }
+
@Override
public ProcessResult runTool(ProcessMode processMode, GenericVersionRange toolVersion, ProcessErrorHandling errorHandling, String... args) {
@@ -63,19 +77,6 @@ protected void setEnvironment(EnvironmentContext environmentContext, ToolInstall
environmentContext.withEnvVar("CATALINA_HOME", toolInstallation.linkDir().toString());
}
- @Override
- protected void postExtract(Path extractedDir) {
-
- super.postExtract(extractedDir);
- String binaryName;
- if (this.context.getSystemInfo().isWindows()) {
- binaryName = "catalina.bat";
- } else {
- binaryName = "catalina.sh";
- }
- createStartScript(extractedDir, binaryName, false);
- }
-
private void printTomcatPort() {
String portNumber = findTomcatPort();
diff --git a/cli/src/main/java/com/devonfw/tools/ide/validation/PropertyValidator.java b/cli/src/main/java/com/devonfw/tools/ide/validation/PropertyValidator.java
index aafaefd03..e1f0da445 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/validation/PropertyValidator.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/validation/PropertyValidator.java
@@ -1,8 +1,17 @@
package com.devonfw.tools.ide.validation;
+/**
+ * {@link FunctionalInterface} for validator of a {@link com.devonfw.tools.ide.property.Property}.
+ *
+ * @param type of the {@link com.devonfw.tools.ide.property.Property#getValue() property value}.
+ */
@FunctionalInterface
public interface PropertyValidator {
+ /**
+ * @param value the value to validate.
+ * @param validationState the {@link ValidationState} where error messages can be added.
+ */
void validate(V value, ValidationState validationState);
}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/validation/ValidationResult.java b/cli/src/main/java/com/devonfw/tools/ide/validation/ValidationResult.java
index c9fe76643..eeb6deb2a 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/validation/ValidationResult.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/validation/ValidationResult.java
@@ -1,9 +1,18 @@
package com.devonfw.tools.ide.validation;
+/**
+ * Interface for the result of a validation.
+ */
public interface ValidationResult {
+ /**
+ * @return {@code true} if the validation was successful, {@code false} otherwise (validation constraint(s) violated).
+ */
boolean isValid();
+ /**
+ * @return the error messsage(s) of the validation or {@code null} if {@link #isValid() valid}.
+ */
String getErrorMessage();
}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/validation/ValidationResultValid.java b/cli/src/main/java/com/devonfw/tools/ide/validation/ValidationResultValid.java
new file mode 100644
index 000000000..f8b201c31
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/validation/ValidationResultValid.java
@@ -0,0 +1,29 @@
+package com.devonfw.tools.ide.validation;
+
+/**
+ * Implementation of {@link ValidationResult} that is always {@link #isValid() valid} and has no {@link #getErrorMessage() error message}.
+ */
+public class ValidationResultValid implements ValidationResult {
+
+ private static final ValidationResultValid INSTANCE = new ValidationResultValid();
+
+ @Override
+ public boolean isValid() {
+
+ return true;
+ }
+
+ @Override
+ public String getErrorMessage() {
+
+ return null;
+ }
+
+ /**
+ * @return the singleton instance of {@link ValidationResultValid}.
+ */
+ public static ValidationResultValid get() {
+
+ return INSTANCE;
+ }
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/validation/ValidationState.java b/cli/src/main/java/com/devonfw/tools/ide/validation/ValidationState.java
index ed165da20..5758143e6 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/validation/ValidationState.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/validation/ValidationState.java
@@ -1,19 +1,34 @@
package com.devonfw.tools.ide.validation;
+/**
+ * Implementation of {@link ValidationResult} as a mutable state that can collect errors dynamically.
+ */
public class ValidationState implements ValidationResult {
+ private final String propertyName;
+
private StringBuilder errorMessage;
- private String propertyName;
+ /**
+ * The default constructor for no property.
+ */
+ public ValidationState() {
+ this(null);
+ }
+ /**
+ * @param propertyName the name of the property to validate.
+ */
public ValidationState(String propertyName) {
this.propertyName = propertyName;
}
+ @Override
public boolean isValid() {
return (this.errorMessage == null);
}
+ @Override
public String getErrorMessage() {
if (this.errorMessage == null) {
return null;
@@ -21,6 +36,9 @@ public String getErrorMessage() {
return this.errorMessage.toString();
}
+ /**
+ * @param error the error message to add to this {@link ValidationState}.
+ */
public void addErrorMessage(String error) {
if (this.errorMessage == null) {
if (this.propertyName == null) {
@@ -37,6 +55,9 @@ public void addErrorMessage(String error) {
this.errorMessage.append(error);
}
+ /**
+ * @param result the {@link ValidationResult} to add to this {@link ValidationState}.
+ */
public void add(ValidationResult result) {
if (!result.isValid()) {
if (this.errorMessage == null) {
diff --git a/cli/src/main/java/com/devonfw/tools/ide/variable/IdeVariables.java b/cli/src/main/java/com/devonfw/tools/ide/variable/IdeVariables.java
index 4ba2ea612..c88080231 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/variable/IdeVariables.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/variable/IdeVariables.java
@@ -3,7 +3,7 @@
import java.util.Collection;
import java.util.List;
-import com.devonfw.tools.ide.context.GitUrlSyntax;
+import com.devonfw.tools.ide.git.GitUrlSyntax;
/**
* Interface (mis)used to define all the available variables.
diff --git a/cli/src/main/package/bin/ide b/cli/src/main/package/bin/ide
deleted file mode 100644
index 3b7f1bdd1..000000000
--- a/cli/src/main/package/bin/ide
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/env bash
-
-_IDEASY="$(dirname "${BASH_SOURCE}")/ideasy"
-if [ $# != 0 ]; then
- "${_IDEASY}" "$@"
- return_code=$?
- if [ $return_code != 0 ]; then
- echo -e "\n\033[91mError: IDEasy failed with exit code ${return_code}\033[91m" >&2
- unset _IDEASY
- return ${return_code}
- fi
-fi
-
-ide_env="$("${_IDEASY}" env --bash)"
-if [ $? = 0 ]; then
- eval "${ide_env}"
- if [ $# = 0 ]; then
- echo "IDE environment variables have been set for ${IDE_HOME} in workspace ${WORKSPACE}"
- fi
-fi
-
-if [ "${OSTYPE}" = "cygwin" ]; then
- echo -e "\033[93m--- WARNING: CYGWIN IS NOT SUPPORTED ---\nCygwin console is not supported by IDEasy.\nConsider using the git terminal instead.\nIf you want to use Cygwin with IDEasy, you will have to configure it yourself.\nA few suggestions and caveats can be found here:\nhttps://github.com/devonfw/IDEasy/blob/main/documentation/cygwin.adoc\n\033[39m"
-fi
-
-unset _IDEASY
-unset ide_env
-unset return_code
diff --git a/cli/src/main/package/bin/ide.bat b/cli/src/main/package/bin/ide.bat
index 30ba76e7b..86ccf60e2 100644
--- a/cli/src/main/package/bin/ide.bat
+++ b/cli/src/main/package/bin/ide.bat
@@ -6,7 +6,7 @@ Set _fBRed=[91m
Set _RESET=[0m
if not "%1%" == "" (
- ideasy %*
+ ideasy %IDE_OPTIONS% %*
if not %ERRORLEVEL% == 0 (
echo %_fBRed%Error: IDEasy failed with exit code %ERRORLEVEL% %_RESET%
exit /b %ERRORLEVEL%
@@ -14,7 +14,7 @@ if not "%1%" == "" (
)
REM https://stackoverflow.com/questions/61888625/what-is-f-in-the-for-loop-command
-for /f "tokens=*" %%i in ('ideasy env') do (
+for /f "tokens=*" %%i in ('ideasy %IDE_OPTIONS% env') do (
call set %%i
)
if not %ERRORLEVEL% == 0 (
diff --git a/cli/src/main/package/completion b/cli/src/main/package/completion
deleted file mode 100755
index 46b6e87b9..000000000
--- a/cli/src/main/package/completion
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env bash
-_ide_completion()
-{
- if [ -z "${COMP_WORDS[COMP_CWORD]}" ]; then
- COMPREPLY=( $(ideasy -q complete ${COMP_WORDS[@]:1} "") )
- else
- COMPREPLY=( $(ideasy -q complete ${COMP_WORDS[@]:1}) )
- fi
-}
-
-complete -F _ide_completion ide
diff --git a/cli/src/main/package/functions b/cli/src/main/package/functions
new file mode 100644
index 000000000..6774de33f
--- /dev/null
+++ b/cli/src/main/package/functions
@@ -0,0 +1,94 @@
+#!/usr/bin/env bash
+# this script should be sourced, shebang above only for syntax highlighting in editors
+function ide() {
+ local return_code
+ local ide_env
+ if [ $# != 0 ] && [ "$1" != "init" ]; then
+ ideasy ${IDE_OPTIONS} "$@"
+ return_code=$?
+ if [ $return_code != 0 ]; then
+ echo -e "\n\033[91mError: IDEasy failed with exit code ${return_code}\033[91m" >&2
+ return ${return_code}
+ fi
+ fi
+ ide_env="$(ideasy ${IDE_OPTIONS} env --bash)"
+ if [ $? = 0 ]; then
+ eval "${ide_env}"
+ if [ $# = 0 ]; then
+ ideasy ${IDE_OPTIONS} status
+ echo "IDE environment variables have been set for ${IDE_HOME} in workspace ${WORKSPACE}"
+ fi
+ fi
+ if [ "${OSTYPE}" = "cygwin" ] && [ "$1" != "init" ]; then
+ echo -e "\033[93m--- WARNING: CYGWIN IS NOT SUPPORTED ---\nCygwin console is not supported by IDEasy.\nConsider using the git terminal instead.\nIf you want to use Cygwin with IDEasy, you will have to configure it yourself.\nA few suggestions and caveats can be found here:\nhttps://github.com/devonfw/IDEasy/blob/main/documentation/cygwin.adoc\n\033[39m"
+ fi
+}
+
+function icd() {
+ if [ $# = 1 ] && [ "${1::1}" != "-" ]; then
+ cd $1 || return 1
+ ide init
+ return
+ elif [ $# -gt 2 ]; then
+ echo -e "\033[91mInvalid usage icd $*\033[39m" >&2
+ return 1
+ fi
+ if [ "$1" = "-p" ]; then
+ if [ -d "${IDE_ROOT}/$2" ]; then
+ cd "${IDE_ROOT}/$2"
+ ide init
+ return
+ else
+ echo -e "\033[93mNo such IDE project ${IDE_ROOT}/$2\033[39m" >&2
+ return 1
+ fi
+ fi
+ if [ ! -d "${IDE_HOME}" ]; then
+ ide init
+ fi
+ if [ $# = 0 ]; then
+ if [ -d "${IDE_HOME}" ]; then
+ cd "${IDE_HOME}"
+ return
+ fi
+ echo -e "\033[93mYou are not inside an IDE project: $PWD\033[39m" >&2
+ return 1
+ elif [ "$1" = "-w" ]; then
+ local wksp=$2
+ if [ "${wksp}" = "" ]; then
+ wksp=main
+ fi
+ if [ -d "${IDE_HOME}/workspaces/${wksp}" ]; then
+ cd "${IDE_HOME}/workspaces/${wksp}"
+ ide init
+ return
+ else
+ echo -e "\033[93mNo such IDE workspace ${IDE_HOME}/workspaces/${wksp}\033[39m" >&2
+ return 1
+ fi
+ fi
+}
+
+_ide_completion()
+{
+ if [ -z "${COMP_WORDS[COMP_CWORD]}" ]; then
+ COMPREPLY=( $(ideasy -q complete ${COMP_WORDS[@]:1} "") )
+ else
+ COMPREPLY=( $(ideasy -q complete ${COMP_WORDS[@]:1}) )
+ fi
+}
+
+if [ "${0/*\//}" = "zsh" ]; then
+ autoload -Uz compinit
+ compinit
+ autoload bashcompinit
+ bashcompinit
+fi
+
+if ! command -v ideasy &> /dev/null; then
+ export PATH="${PATH}:${IDE_ROOT}/_ide/bin"
+fi
+
+complete -F _ide_completion ide
+ide init
+
diff --git a/cli/src/main/package/setup b/cli/src/main/package/setup
index d7fa8828c..c37cef0ee 100755
--- a/cli/src/main/package/setup
+++ b/cli/src/main/package/setup
@@ -6,34 +6,30 @@ function doSetupInConfigFile() {
echo "${cfg} not found - skipping."
return
fi
- if [ "${cfg}" = "~/.zshrc" ]; then
- if ! grep -q "compinit" "${cfg}"; then
- echo -e 'autoload -Uz compinit\ncompinit' >> "${cfg}"
- fi
- if ! grep -q "bashcompinit" "${cfg}"; then
- echo -e 'autoload bashcompinit\nbashcompinit' >> "${cfg}"
- fi
- fi
echo "Configuring IDEasy in ${cfg}."
- if ! grep -q "${AUTOCOMPLETION}" "${cfg}"; then
- echo -e "${AUTOCOMPLETION}" >> "${cfg}"
- fi
- if ! grep -q "alias ide=" "${cfg}"; then
- echo -e "alias ide=\"source ${PWD}/bin/ide\"" >> "${cfg}"
- echo -e "ide" >> "${cfg}"
- fi
if [ "${OSTYPE}" != "cygwin" ] && [ "${OSTYPE}" != "msys" ]; then
- if ! grep -q "IDE_ROOT" "${cfg}"
+ if ! grep -q "export IDE_ROOT=" "${cfg}"
then
- echo -e 'export IDE_ROOT="${PWD}"' >> "${cfg}"
+ echo "export IDE_ROOT=\"${IDE_ROOT}\"" >> "${cfg}"
fi
fi
+ if ! grep -q 'source "$IDE_ROOT/_ide/functions"' "${cfg}"; then
+ echo 'source "$IDE_ROOT/_ide/functions"' >> "${cfg}"
+ echo "ide init" >> "${cfg}"
+ fi
}
cd "$(dirname "${BASH_SOURCE:-$0}")" || exit 255
-echo "Setting up your IDEasy in ${PWD}"
-
-AUTOCOMPLETION="source ${PWD}/completion"
+if [ "${PWD/*\//}" != "_ide" ]; then
+ echo -e "\033[93mInvalid installation path $PWD - you need to install IDEasy to a folder named '_ide'.\033[39m" >&2
+ exit 1
+fi
+echo "Setting up IDEasy in ${PWD}"
+cd ..
+export IDE_ROOT=${PWD}
+source "$IDE_ROOT/_ide/functions"
doSetupInConfigFile ~/.bashrc
doSetupInConfigFile ~/.zshrc
+
+echo -e "\033[93mATTENTION: IDEasy has been setup for your shells but you need to start a new shell to make it work.\nOnly if you invoked this setup script by sourcing it, you are able to run 'ide' and 'icd' commands without starting a new shell.\n\033[39m" >&2
diff --git a/cli/src/main/resources/META-INF/native-image/com.devonfw.tools.IDEasy/ide-cli/resource-config.json b/cli/src/main/resources/META-INF/native-image/com.devonfw.tools.IDEasy/ide-cli/resource-config.json
new file mode 100644
index 000000000..fbd60b774
--- /dev/null
+++ b/cli/src/main/resources/META-INF/native-image/com.devonfw.tools.IDEasy/ide-cli/resource-config.json
@@ -0,0 +1,7 @@
+{
+ "resources": {
+ "includes": [
+ {"pattern": "nls/.*"}
+ ]
+ }
+}
diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties
index 0ebd40e67..42486803f 100644
--- a/cli/src/main/resources/nls/Help.properties
+++ b/cli/src/main/resources/nls/Help.properties
@@ -1,5 +1,3 @@
-cmd.--version=Print the version of IDEasy.
-cmd.--version.detail=To print the current version of IDEasy simply type: 'ide --version'.
cmd.android-studio=Tool commandlet for Android Studio (IDE).
cmd.android-studio.detail=The android-studio commandlet allows to install, configure, and launch Android Studio. To launch Android Studio for your current workspace and IDEasy installation, simply run: ide android-studio. Detailed documentation can be found at https://developer.android.com/studio/.
cmd.aws=Tool commandlet for AWS CLI.
@@ -62,6 +60,8 @@ cmd.kotlinc=Tool commandlet for Kotlin (compiler for JRE language).
cmd.kotlinc.detail=Kotlin Compiler (kotlinc) is the command-line tool for compiling Kotlin code. Detailed documentation can be found at https://kotlinlang.org/docs/home.html
cmd.kotlincnative=Tool commandlet for Kotlin-Native (compiler for JRE language).
cmd.kotlincnative.detail=Kotlin/Native Compiler (kotlincnative) compiles Kotlin code to native executables. Detailed documentation can be found at https://kotlinlang.org/docs/reference/native-overview.html
+cmd.kubectl=Tool commandlet for kubernetes. Detailed documentation can be found at https://kubernetes.io/docs/home/
+cmd.kubectl.detail=The kubectl commandlet allows to install and use kubernetes. On Windows WSL 2 (Windows Subsystem for Linux) has to be installed properly as a prerequisite.
cmd.lazydocker=Tool commandlet for LazyDocker.
cmd.lazydocker.detail=Lazydocker is a simple terminal UI for both docker and docker-compose. Detailed documentation can be found at https://github.com/jesseduffield/lazydocker
cmd.list-editions=List the available editions of the selected tool.
@@ -95,6 +95,8 @@ cmd.shell.detail=The interactive shell offers console users a new user experienc
cmd.sonar=Tool commandlet for SonarQube.
cmd.sonar.detail=SonarQube is a platform for continuous inspection of code quality. Detailed documentation can be found at https://docs.sonarqube.org/
cmd.sonar.val.command=Action to perform (START|STOP|ANALYZE)
+cmd.status=Prints the status report about your IDEasy.
+cmd.status.detail=To check your IDE-status (e.g. duplicated or legacy variables) as well as potential information about updates to settings you should apply\nwith ide update, run the following command: 'ide status'.
cmd.terraform=Tool commandlet for Terraform.
cmd.terraform.detail=Terraform is an infrastructure as code tool for managing cloud resources. Detailed documentation can be found at https://www.terraform.io/docs/index.html
cmd.tomcat=Tool commandlet for Tomcat
@@ -106,6 +108,8 @@ cmd.uninstall-plugin.detail=Plugins can be only installed or uninstalled for too
cmd.uninstall.detail=Can be used to uninstall any tool e.g. to uninstall java simply type: 'uninstall java'.
cmd.update=Pull your settings and apply updates (software, configuration and repositories).
cmd.update.detail=To update your IDE (if instructed by your ide-admin), you only need to run the following command: 'ide update'.
+cmd.version=Print the version of IDEasy.
+cmd.version.detail=To print the current version of IDEasy simply type: 'ide --version'.
cmd.vscode=Tool commandlet for Visual Studio Code (IDE).
cmd.vscode.detail=Visual Studio Code (VS Code) is a popular code editor developed by Microsoft. Detailed documentation can be found at https://code.visualstudio.com/docs/
commandlets=Available commandlets:
@@ -117,6 +121,7 @@ opt.--offline=enable offline mode (skip updates or git pull, fail downloads or g
opt.--quiet=disable info logging (only log success, warning or error).
opt.--skip-repositories=skip the setup of repositories.
opt.--skip-tools=skip the installation/update of tools.
+opt.--skip-updates=disables tool updates if the configured versions match the installed versions.
opt.--trace=enable trace logging.
opt.--version=Print the IDE version and exit.
options=Options:
diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties
index efab2481a..3e7af8e8c 100644
--- a/cli/src/main/resources/nls/Help_de.properties
+++ b/cli/src/main/resources/nls/Help_de.properties
@@ -1,5 +1,3 @@
-cmd.--version=Gibt die Version von IDEasy aus.
-cmd.--version.detail=Um die aktuelle Version von IDEasy auszugeben, brauchen Sie einfach nur 'ide --version' in die Konsole eingeben.
cmd.android-studio=Werkzeug Kommando für Android Studio (IDE).
cmd.android-studio.detail=Das android-studio Kommando ermöglicht die Installation, Konfiguration und den Start von Android Studio. Um Android Studio für Ihren aktuellen Arbeitsbereich und die IDEasy-Installation zu starten, führen Sie einfach: 'ide android-studio' aus. Detaillierte Dokumentation finden Sie unter https://developer.android.com/studio/.
cmd.aws=Werkzeug Kommando für AWS Kommandoschnittstelle.
@@ -45,7 +43,7 @@ cmd.help=Zeigt diese Hilfe an.
cmd.help.detail=Die Hilfe-Details eines Kommandos können mit dem Befehl "ide help " ausgegeben werden.
cmd.install=Installiert das selektierte Werkzeug.
cmd.install-plugin=Installiert die selektierte Erweiterung für das selektierte Werkzeug.
-cmd.install-plugin.detail=Erweiterung können nur für Werkzeuge installiert werden die diese unterstützen. Erweiterungen müssen in den Projekteinstellungen definiert sein um zur Installation verfügbar zu sein. Falls eine gewünschte Erweiterung nicht verfügbar ist, bitte den ide-admin kontaktieren.
+cmd.install-plugin.detail=Erweiterung können nur für Werkzeuge installiert werden, die diese unterstützen. Erweiterungen müssen in den Projekteinstellungen definiert sein um zur Installation verfügbar zu sein. Falls eine gewünschte Erweiterung nicht verfügbar ist, bitte den ide-admin kontaktieren.
cmd.install.detail=Die Liste an zur Installation verfügbaren Werkzeugen können mit dem Befehl "ide help" ausgegeben werden.
cmd.intellij=Werkzeug Kommando für Intellij (IDE)
cmd.intellij.detail=IntelliJ ist eine beliebte Java-Entwicklungsumgebung, von JetBrains entwickelt. Detaillierte Dokumentation ist zu finden unter https://www.jetbrains.com/idea/documentation/
@@ -62,6 +60,8 @@ cmd.kotlinc=Werkzeug Kommando für Kotlin (Compiler für JRE Sprache).
cmd.kotlinc.detail=Der Kotlin-Compiler (kotlinc) ist das Befehlszeilentool zum Kompilieren von Kotlin-Code. Detaillierte Dokumentation ist zu finden unter https://kotlinlang.org/docs/home.html
cmd.kotlincnative=Werkzeug Kommando für Kotlin-Native (Compiler für JRE Sprache).
cmd.kotlincnative.detail=Der Kotlin/Native-Compiler (kotlincnative) kompiliert Kotlin-Code in native ausführbare Dateien. Detaillierte Dokumentation ist zu finden unter https://kotlinlang.org/docs/reference/native-overview.html
+cmd.kubectl=Werkzeug Kommando für Kubernetes. Detaillierte Dokumentation ist zu finden unter https://kubernetes.io/docs/home/
+cmd.kubectl.detail=Der Befehl kubectl ermöglicht die Installation und Nutzung von Kubernetes. Unter Windows muss WSL 2 (Windows Subsystem for Linux) ordnungsgemäß installiert sein.
cmd.lazydocker=Werkzeug Kommando für LazyDocker.
cmd.lazydocker.detail=Lazydocker ist ein einfaches Kommandozeilen-Benutzer-Interface für Docker und Docker-compose. Detaillierte Dokumentation ist zu finden unter https://github.com/jesseduffield/lazydocker
cmd.list-editions=Listet die verfügbaren Editionen des selektierten Werkzeugs auf.
@@ -95,6 +95,8 @@ cmd.shell.detail=Die integrierte Shell bietet Konsolenbenutzer*Innen eine neue B
cmd.sonar=Werkzeug Kommando für SonarQube.
cmd.sonar.detail=SonarQube ist eine Plattform für die kontinuierliche Code-Qualitätsprüfung. Detaillierte Dokumentation ist zu finden unter https://docs.sonarqube.org/
cmd.sonar.val.command=Auszuführende Aktion (START|STOP|ANALYZE)
+cmd.status=Gibt einen Statusbericht über IDEasy aus
+cmd.status.detail=Um den Status Ihrer IDE zu überprüfen (z.B. doppelte oder veraltete Variablen) sowie potenzielle Informationen über Updates zu den Einstellungen,\ndie Sie mit dem Befehl 'ide update' anwenden sollten, führen Sie den folgenden Befehl aus: 'ide status'.
cmd.terraform=Werkzeug Kommando für Terraform.
cmd.terraform.detail=Terraform ist ein Tool für Infrastructure as Code zur Verwaltung von Cloud-Ressourcen. Detaillierte Dokumentation ist zu finden unter https://www.terraform.io/docs/index.html
cmd.tomcat=Werkzeug Kommando für Tomcat
@@ -106,6 +108,8 @@ cmd.uninstall-plugin.detail=Erweiterung können nur für Werkzeuge installiert u
cmd.uninstall.detail=Wird dazu verwendet um jedwedes Werkzeug zu deinstallieren. Um z.B. Java zu deinstallieren geben Sie einfach 'uninstall java' in die Konsole ein.
cmd.update=Updatet die Settings, Software und Repositories.
cmd.update.detail=Um die IDE auf den neuesten Stand zu bringen (falls von Ihrem Admin angewiesen) geben Sie einfach 'ide update' in die Konsole ein.
+cmd.version=Gibt die Version von IDEasy aus.
+cmd.version.detail=Um die aktuelle Version von IDEasy auszugeben, brauchen Sie einfach nur 'ide --version' in die Konsole eingeben.
cmd.vscode=Werkzeug Kommando für Visual Studio Code (IDE).
cmd.vscode.detail=Visual Studio Code (VS Code) ist ein beliebter Code-Editor, der von Microsoft entwickelt wurde. Detaillierte Dokumentation ist zu finden unter https://code.visualstudio.com/docs/
commandlets=Verfügbare Kommandos:
@@ -117,6 +121,7 @@ opt.--offline=Aktiviert den Offline-Modus (Überspringt Aktualisierungen oder gi
opt.--quiet=Deaktiviert Info Logging ( nur success, warning und error).
opt.--skip-repositories=Überspringt die Einrichtung der Repositories.
opt.--skip-tools=Überspringt die Installation/Aktualisierung der Tools.
+opt.--skip-updates=Deaktiviert Aktualisierungen von Tools wenn die installierten Versionen mit den konfigurierten Versionen übereinstimmen.
opt.--trace=Aktiviert Trace-Ausgaben (detaillierte Fehleranalyse).
opt.--version=Zeigt die IDE Version an und beendet das Programm.
options=Optionen:
diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/EditionGetCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/EditionGetCommandletTest.java
index d751a340d..bb0c3d53a 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/EditionGetCommandletTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/EditionGetCommandletTest.java
@@ -7,6 +7,8 @@
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.log.IdeLogEntry;
+import com.devonfw.tools.ide.log.IdeLogLevel;
/** Integration test of {@link EditionGetCommandlet}. */
@@ -56,7 +58,7 @@ public void testEditionGetCommandletConfiguredEdition() {
// act
editionGet.run();
// assert
- assertThat(context).logAtInfo().hasMessage("java");
+ assertThat(context).log(IdeLogLevel.PROCESSABLE).hasMessage("java");
}
/** Test of {@link EditionGetCommandlet} run, when tool is not installed. */
@@ -70,10 +72,10 @@ public void testEditionGetCommandletToolNotInstalled() {
editionGet.tool.setValueAsString("az", context);
editionGet.run();
// assert
- assertThat(context).logAtInfo().hasEntries("No installation of tool az was found.",
- "The configured edition for tool az is az",
- "To install that edition call the following command:",
- "ide install az");
+ assertThat(context).log().hasEntries(IdeLogEntry.ofWarning("No installation of tool az was found."),
+ IdeLogEntry.ofInfo("The configured edition for tool az is az"),
+ IdeLogEntry.ofInfo("To install that edition call the following command:"),
+ IdeLogEntry.ofInfo("ide install az"));
}
/** Test of {@link EditionGetCommandlet} run, with --installed flag, when Installed Version is null. */
@@ -88,10 +90,10 @@ public void testEditionGetCommandletInstalledEditionToolNotInstalled() {
// act
editionGet.run();
// assert
- assertThat(context).logAtInfo().hasEntries("No installation of tool java was found.",
- "The configured edition for tool java is java",
- "To install that edition call the following command:",
- "ide install java");
+ assertThat(context).log().hasEntries(IdeLogEntry.ofWarning("No installation of tool java was found."),
+ IdeLogEntry.ofInfo("The configured edition for tool java is java"),
+ IdeLogEntry.ofInfo("To install that edition call the following command:"),
+ IdeLogEntry.ofInfo("ide install java"));
}
@@ -107,6 +109,6 @@ public void testEditionGetCommandletInstalledEditionToolInstalled() {
editionGet.installed.setValue(true);
editionGet.run();
// assert
- assertThat(context).logAtInfo().hasMessage("mvn");
+ assertThat(context).log(IdeLogLevel.PROCESSABLE).hasMessage("mvn");
}
}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/EnvironmentCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/EnvironmentCommandletTest.java
index 0140a490d..0258febcd 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/EnvironmentCommandletTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/EnvironmentCommandletTest.java
@@ -6,10 +6,9 @@
import com.devonfw.tools.ide.context.AbstractIdeContextTest;
import com.devonfw.tools.ide.context.IdeTestContext;
-import com.devonfw.tools.ide.context.IdeTestContextMock;
import com.devonfw.tools.ide.log.IdeLogEntry;
import com.devonfw.tools.ide.log.IdeLogLevel;
-import com.devonfw.tools.ide.os.SystemInfoImpl;
+import com.devonfw.tools.ide.os.SystemInfoMock;
/**
* Test of {@link EnvironmentCommandlet}.
@@ -25,6 +24,7 @@ public void testRunDebugLogging() {
// arrange
String path = "project/workspaces/foo-test/my-git-repo";
IdeTestContext context = newContext(PROJECT_BASIC, path, false);
+ context.setSystemInfo(SystemInfoMock.LINUX_X64);
EnvironmentCommandlet env = context.getCommandletManager().getCommandlet(EnvironmentCommandlet.class);
Path userProperties = context.getUserHomeIde().resolve("ide.properties");
Path settingsIdeProperties = context.getSettingsPath().resolve("ide.properties");
@@ -35,43 +35,43 @@ public void testRunDebugLogging() {
// assert
assertThat(context).log().hasEntriesWithNothingElseInBetween( //
IdeLogEntry.ofDebug("from USER@" + userProperties.toString() + ":"), //
- IdeLogEntry.ofInfo("DOCKER_EDITION=\"docker\""), //
- IdeLogEntry.ofInfo("FOO=\"foo-bar-some-${UNDEFINED}\""), //
+ IdeLogEntry.ofProcessable("DOCKER_EDITION=\"docker\""), //
+ IdeLogEntry.ofProcessable("FOO=\"foo-bar-some-${UNDEFINED}\""), //
IdeLogEntry.ofDebug("from SETTINGS@" + settingsIdeProperties + ":"),
- IdeLogEntry.ofInfo("BAR=\"bar-some-${UNDEFINED}\""), //
- IdeLogEntry.ofInfo("ECLIPSE_VERSION=\"2023-03\""), //
- IdeLogEntry.ofInfo("IDE_TOOLS=\"mvn,eclipse\""), //
- IdeLogEntry.ofInfo("INTELLIJ_EDITION=\"ultimate\""), //
- IdeLogEntry.ofInfo("JAVA_VERSION=\"17*\""), //
- IdeLogEntry.ofInfo("TEST_ARGS4=\" settings4\""), //
- IdeLogEntry.ofInfo("TEST_ARGSb=\"user10 workspace10 settingsb user1 settings1 workspace1 conf1 user3 workspace3 confa userb\""), //
+ IdeLogEntry.ofProcessable("BAR=\"bar-some-${UNDEFINED}\""), //
+ IdeLogEntry.ofProcessable("ECLIPSE_VERSION=\"2023-03\""), //
+ IdeLogEntry.ofProcessable("IDE_TOOLS=\"mvn,eclipse\""), //
+ IdeLogEntry.ofProcessable("INTELLIJ_EDITION=\"ultimate\""), //
+ IdeLogEntry.ofProcessable("JAVA_VERSION=\"17*\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS4=\" settings4\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGSb=\"user10 workspace10 settingsb user1 settings1 workspace1 conf1 user3 workspace3 confa userb\""), //
IdeLogEntry.ofDebug("from WORKSPACE@" + workspaceIdeProperties + ":"), //
- IdeLogEntry.ofInfo("TEST_ARGS10=\"user10 workspace10\""), //
- IdeLogEntry.ofInfo("TEST_ARGS3=\" user3 workspace3\""), //
- IdeLogEntry.ofInfo("TEST_ARGS9=\"settings9 workspace9\""), //
- IdeLogEntry.ofInfo("TEST_ARGSd=\" user1 settings1 workspace1 conf1 userd workspaced\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS10=\"user10 workspace10\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS3=\" user3 workspace3\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS9=\"settings9 workspace9\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGSd=\" user1 settings1 workspace1 conf1 userd workspaced\""), //
IdeLogEntry.ofDebug("from CONF@" + confIdeProperties + ":"), //
- IdeLogEntry.ofInfo("MVN_VERSION=\"3.9.1\""), //
- IdeLogEntry.ofInfo("SOME=\"some-${UNDEFINED}\""), //
- IdeLogEntry.ofInfo("TEST_ARGS1=\" user1 settings1 workspace1 conf1\""), //
- IdeLogEntry.ofInfo("TEST_ARGS2=\" user2 conf2\""), //
- IdeLogEntry.ofInfo("TEST_ARGS5=\" settings5 conf5\""), //
- IdeLogEntry.ofInfo("TEST_ARGS6=\" settings6 workspace6 conf6\""), //
- IdeLogEntry.ofInfo("TEST_ARGS7=\"user7 settings7 workspace7 conf7\""), //
- IdeLogEntry.ofInfo("TEST_ARGS8=\"settings8 workspace8 conf8\""), //
- IdeLogEntry.ofInfo("TEST_ARGSa=\" user1 settings1 workspace1 conf1 user3 workspace3 confa\""), //
- IdeLogEntry.ofInfo("TEST_ARGSc=\" user1 settings1 workspace1 conf1 userc settingsc confc\""), //
+ IdeLogEntry.ofProcessable("MVN_VERSION=\"3.9.1\""), //
+ IdeLogEntry.ofProcessable("SOME=\"some-${UNDEFINED}\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS1=\" user1 settings1 workspace1 conf1\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS2=\" user2 conf2\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS5=\" settings5 conf5\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS6=\" settings6 workspace6 conf6\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS7=\"user7 settings7 workspace7 conf7\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS8=\"settings8 workspace8 conf8\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGSa=\" user1 settings1 workspace1 conf1 user3 workspace3 confa\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGSc=\" user1 settings1 workspace1 conf1 userc settingsc confc\""), //
IdeLogEntry.ofDebug("from RESOLVED:"), //
- IdeLogEntry.ofInfo("HOME=\"" + context.getUserHome() + "\""), //
- IdeLogEntry.ofInfo("IDE_HOME=\"" + context.getIdeHome() + "\""), //
- IdeLogEntry.ofInfo("export M2_REPO=\"" + context.getUserHome() + "/.m2/repository\""), //
- new IdeLogEntry(IdeLogLevel.INFO, "export PATH=", true), //
- IdeLogEntry.ofInfo("WORKSPACE=\"foo-test\""), //
- IdeLogEntry.ofInfo("WORKSPACE_PATH=\"" + context.getWorkspacePath() + "\"") //
+ IdeLogEntry.ofProcessable("HOME=\"" + normalize(context.getUserHome()) + "\""), //
+ IdeLogEntry.ofProcessable("IDE_HOME=\"" + normalize(context.getIdeHome()) + "\""), //
+ IdeLogEntry.ofProcessable("export M2_REPO=\"" + context.getUserHome() + "/.m2/repository\""), //
+ new IdeLogEntry(IdeLogLevel.PROCESSABLE, "export PATH=", true), //
+ IdeLogEntry.ofProcessable("WORKSPACE=\"foo-test\""), //
+ IdeLogEntry.ofProcessable("WORKSPACE_PATH=\"" + normalize(context.getWorkspacePath()) + "\"") //
);
}
@@ -84,6 +84,7 @@ public void testRunInfoLogging() {
// arrange
String path = "project/workspaces/foo-test/my-git-repo";
IdeTestContext context = newContext(PROJECT_BASIC, path, false, IdeLogLevel.INFO);
+ context.setSystemInfo(SystemInfoMock.MAC_ARM64);
EnvironmentCommandlet env = context.getCommandletManager().getCommandlet(EnvironmentCommandlet.class);
Path userProperties = context.getUserHomeIde().resolve("ide.properties");
Path settingsIdeProperties = context.getSettingsPath().resolve("ide.properties");
@@ -92,82 +93,41 @@ public void testRunInfoLogging() {
// act
env.run();
// assert
- if (SystemInfoImpl.INSTANCE.isWindows()) {
- assertThat(context).log().hasEntries /*WithNothingElseInBetween*/( //
- IdeLogEntry.ofInfo("BAR=bar-some-${UNDEFINED}"), //
- IdeLogEntry.ofInfo("DOCKER_EDITION=docker"), //
- IdeLogEntry.ofInfo("ECLIPSE_VERSION=2023-03"), //
- IdeLogEntry.ofInfo("FOO=foo-bar-some-${UNDEFINED}"), //
- IdeLogEntry.ofInfo("HOME=" + context.getUserHome() + ""), //
- IdeLogEntry.ofInfo("IDE_HOME=" + context.getIdeHome() + ""), //
- IdeLogEntry.ofInfo("IDE_TOOLS=mvn,eclipse"), //
- IdeLogEntry.ofInfo("INTELLIJ_EDITION=ultimate"), //
- IdeLogEntry.ofInfo("JAVA_VERSION=17*"), //
- IdeLogEntry.ofInfo("M2_REPO=" + context.getUserHome() + "/.m2/repository"), //
- IdeLogEntry.ofInfo("MVN_VERSION=3.9.1"), //
- new IdeLogEntry(IdeLogLevel.INFO, "PATH=", true), //
- IdeLogEntry.ofInfo("SOME=some-${UNDEFINED}"), //
- IdeLogEntry.ofInfo("TEST_ARGS1= user1 settings1 workspace1 conf1"), //
- IdeLogEntry.ofInfo("TEST_ARGS10=user10 workspace10"), //
- IdeLogEntry.ofInfo("TEST_ARGS2= user2 conf2"), //
- IdeLogEntry.ofInfo("TEST_ARGS3= user3 workspace3"), //
- IdeLogEntry.ofInfo("TEST_ARGS4= settings4"), //
- IdeLogEntry.ofInfo("TEST_ARGS5= settings5 conf5"), //
- IdeLogEntry.ofInfo("TEST_ARGS6= settings6 workspace6 conf6"), //
- IdeLogEntry.ofInfo("TEST_ARGS7=user7 settings7 workspace7 conf7"), //
- IdeLogEntry.ofInfo("TEST_ARGS8=settings8 workspace8 conf8"), //
- IdeLogEntry.ofInfo("TEST_ARGS9=settings9 workspace9"), //
- IdeLogEntry.ofInfo("TEST_ARGSa= user1 settings1 workspace1 conf1 user3 workspace3 confa"), //
- IdeLogEntry.ofInfo("TEST_ARGSb=user10 workspace10 settingsb user1 settings1 workspace1 conf1 user3 workspace3 confa userb"), //
- IdeLogEntry.ofInfo("TEST_ARGSc= user1 settings1 workspace1 conf1 userc settingsc confc"), //
- IdeLogEntry.ofInfo("TEST_ARGSd= user1 settings1 workspace1 conf1 userd workspaced"), //
- IdeLogEntry.ofInfo("WORKSPACE=foo-test"), //
- IdeLogEntry.ofInfo("WORKSPACE_PATH=" + context.getWorkspacePath()) //
- );
- } else {
- assertThat(context).log().hasEntriesWithNothingElseInBetween( //
- IdeLogEntry.ofInfo("BAR=\"bar-some-${UNDEFINED}\""), //
- IdeLogEntry.ofInfo("DOCKER_EDITION=\"docker\""), //
- IdeLogEntry.ofInfo("ECLIPSE_VERSION=\"2023-03\""), //
- IdeLogEntry.ofInfo("FOO=\"foo-bar-some-${UNDEFINED}\""), //
- IdeLogEntry.ofInfo("HOME=\"" + context.getUserHome() + "\""), //
- IdeLogEntry.ofInfo("IDE_HOME=\"" + context.getIdeHome() + "\""), //
- IdeLogEntry.ofInfo("IDE_TOOLS=\"mvn,eclipse\""), //
- IdeLogEntry.ofInfo("INTELLIJ_EDITION=\"ultimate\""), //
- IdeLogEntry.ofInfo("JAVA_VERSION=\"17*\""), //
- IdeLogEntry.ofInfo("export M2_REPO=\"" + context.getUserHome() + "/.m2/repository\""), //
- IdeLogEntry.ofInfo("MVN_VERSION=\"3.9.1\""), //
- new IdeLogEntry(IdeLogLevel.INFO, "export PATH=", true), //
- IdeLogEntry.ofInfo("SOME=\"some-${UNDEFINED}\""), //
- IdeLogEntry.ofInfo("TEST_ARGS1=\" user1 settings1 workspace1 conf1\""), //
- IdeLogEntry.ofInfo("TEST_ARGS10=\"user10 workspace10\""), //
- IdeLogEntry.ofInfo("TEST_ARGS2=\" user2 conf2\""), //
- IdeLogEntry.ofInfo("TEST_ARGS3=\" user3 workspace3\""), //
- IdeLogEntry.ofInfo("TEST_ARGS4=\" settings4\""), //
- IdeLogEntry.ofInfo("TEST_ARGS5=\" settings5 conf5\""), //
- IdeLogEntry.ofInfo("TEST_ARGS6=\" settings6 workspace6 conf6\""), //
- IdeLogEntry.ofInfo("TEST_ARGS7=\"user7 settings7 workspace7 conf7\""), //
- IdeLogEntry.ofInfo("TEST_ARGS8=\"settings8 workspace8 conf8\""), //
- IdeLogEntry.ofInfo("TEST_ARGS9=\"settings9 workspace9\""), //
- IdeLogEntry.ofInfo("TEST_ARGSa=\" user1 settings1 workspace1 conf1 user3 workspace3 confa\""), //
- IdeLogEntry.ofInfo("TEST_ARGSb=\"user10 workspace10 settingsb user1 settings1 workspace1 conf1 user3 workspace3 confa userb\""), //
- IdeLogEntry.ofInfo("TEST_ARGSc=\" user1 settings1 workspace1 conf1 userc settingsc confc\""), //
- IdeLogEntry.ofInfo("TEST_ARGSd=\" user1 settings1 workspace1 conf1 userd workspaced\""), //
- IdeLogEntry.ofInfo("WORKSPACE=\"foo-test\""), //
- IdeLogEntry.ofInfo("WORKSPACE_PATH=\"" + context.getWorkspacePath() + "\"") //
- );
- }
+ assertThat(context).log().hasEntriesWithNothingElseInBetween( //
+ IdeLogEntry.ofProcessable("BAR=\"bar-some-${UNDEFINED}\""), //
+ IdeLogEntry.ofProcessable("DOCKER_EDITION=\"docker\""), //
+ IdeLogEntry.ofProcessable("ECLIPSE_VERSION=\"2023-03\""), //
+ IdeLogEntry.ofProcessable("FOO=\"foo-bar-some-${UNDEFINED}\""), //
+ IdeLogEntry.ofProcessable("HOME=\"" + normalize(context.getUserHome()) + "\""), //
+ IdeLogEntry.ofProcessable("IDE_HOME=\"" + normalize(context.getIdeHome()) + "\""), //
+ IdeLogEntry.ofProcessable("IDE_TOOLS=\"mvn,eclipse\""), //
+ IdeLogEntry.ofProcessable("INTELLIJ_EDITION=\"ultimate\""), //
+ IdeLogEntry.ofProcessable("JAVA_VERSION=\"17*\""), //
+ IdeLogEntry.ofProcessable("export M2_REPO=\"" + context.getUserHome() + "/.m2/repository\""), //
+ IdeLogEntry.ofProcessable("MVN_VERSION=\"3.9.1\""), //
+ new IdeLogEntry(IdeLogLevel.PROCESSABLE, "export PATH=", true), //
+ IdeLogEntry.ofProcessable("SOME=\"some-${UNDEFINED}\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS1=\" user1 settings1 workspace1 conf1\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS10=\"user10 workspace10\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS2=\" user2 conf2\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS3=\" user3 workspace3\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS4=\" settings4\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS5=\" settings5 conf5\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS6=\" settings6 workspace6 conf6\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS7=\"user7 settings7 workspace7 conf7\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS8=\"settings8 workspace8 conf8\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGS9=\"settings9 workspace9\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGSa=\" user1 settings1 workspace1 conf1 user3 workspace3 confa\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGSb=\"user10 workspace10 settingsb user1 settings1 workspace1 conf1 user3 workspace3 confa userb\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGSc=\" user1 settings1 workspace1 conf1 userc settingsc confc\""), //
+ IdeLogEntry.ofProcessable("TEST_ARGSd=\" user1 settings1 workspace1 conf1 userd workspaced\""), //
+ IdeLogEntry.ofProcessable("WORKSPACE=\"foo-test\""), //
+ IdeLogEntry.ofProcessable("WORKSPACE_PATH=\"" + normalize(context.getWorkspacePath()) + "\"") //
+ );
}
- /**
- * Test that {@link EnvironmentCommandlet} requires home.
- */
- @Test
- public void testThatHomeIsRequired() {
+ private String normalize(Path path) {
- // arrange
- EnvironmentCommandlet env = new EnvironmentCommandlet(IdeTestContextMock.get());
- // act & assert
- assertThat(env.isIdeHomeRequired()).isTrue();
+ return path.toString().replace('\\', '/');
}
}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/HelpCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/HelpCommandletTest.java
index 27797e574..02b1d599f 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/HelpCommandletTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/HelpCommandletTest.java
@@ -146,6 +146,7 @@ private void assertOptionLogMessages(IdeTestContext context) {
assertThat(context).logAtInfo().hasEntries(
"--locale the locale (e.g. '--locale=de' for German language).",
+ "--skip-updates disables tool updates if the configured versions match the installed versions.",
"-b | --batch enable batch mode (non-interactive).",
"-d | --debug enable debug logging.",
"-f | --force enable force mode.",
diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/TestCommandletManager.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/TestCommandletManager.java
new file mode 100644
index 000000000..33c457c74
--- /dev/null
+++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/TestCommandletManager.java
@@ -0,0 +1,25 @@
+package com.devonfw.tools.ide.commandlet;
+
+import com.devonfw.tools.ide.context.IdeContext;
+
+/**
+ * Extends {@link CommandletManagerImpl} to make {@link #add(Commandlet)} method visible for testing and mocking.
+ */
+public class TestCommandletManager extends CommandletManagerImpl {
+
+ /**
+ * @param context the {@link IdeContext}.
+ */
+ public TestCommandletManager(IdeContext context) {
+
+ super(context);
+ }
+
+ @Override
+ public void add(Commandlet commandlet) {
+
+ super.add(commandlet);
+ }
+
+
+}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/VersionGetCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/VersionGetCommandletTest.java
index 6f0bd64dd..c09c09c1f 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/VersionGetCommandletTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/VersionGetCommandletTest.java
@@ -4,6 +4,7 @@
import com.devonfw.tools.ide.context.AbstractIdeContextTest;
import com.devonfw.tools.ide.context.IdeTestContext;
+import com.devonfw.tools.ide.log.IdeLogLevel;
/**
* Integration test of {@link VersionGetCommandlet}.
@@ -62,7 +63,7 @@ public void testVersionGetCommandletConfiguredRun() {
versionGet.configured.setValue(true);
versionGet.run();
// assert
- assertThat(context).logAtInfo().hasMessage("3.9.1");
+ assertThat(context).log(IdeLogLevel.PROCESSABLE).hasMessage("3.9.1");
}
/**
@@ -79,7 +80,7 @@ public void testVersionGetCommandletInstalledRun() {
versionGet.installed.setValue(true);
versionGet.run();
// assert
- assertThat(context).logAtInfo().hasMessage("3.9.4");
+ assertThat(context).log(IdeLogLevel.PROCESSABLE).hasMessage("3.9.4");
}
/**
@@ -96,7 +97,7 @@ public void testVersionGetCommandletConfiguredStarRun() {
versionGet.configured.setValue(true);
versionGet.run();
// assert
- assertThat(context).logAtInfo().hasMessage("*");
+ assertThat(context).log(IdeLogLevel.PROCESSABLE).hasMessage("*");
}
/**
@@ -113,7 +114,7 @@ public void testVersionGetCommandletMatchInstalledToConfiguredStarRun() {
versionGet.tool.setValueAsString("mvn", context);
versionGet.run();
// assert
- assertThat(context).logAtInfo().hasMessage("3.9.4");
+ assertThat(context).log(IdeLogLevel.PROCESSABLE).hasMessage("3.9.4");
}
/**
diff --git a/cli/src/test/java/com/devonfw/tools/ide/completion/CompleteTest.java b/cli/src/test/java/com/devonfw/tools/ide/completion/CompleteTest.java
index 828e9e84c..9ccb62a4f 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/completion/CompleteTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/completion/CompleteTest.java
@@ -11,6 +11,7 @@
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
import com.devonfw.tools.ide.context.AbstractIdeContext;
import com.devonfw.tools.ide.context.IdeContextTest;
+import com.devonfw.tools.ide.property.KeywordProperty;
import com.devonfw.tools.ide.property.Property;
/**
@@ -35,6 +36,23 @@ public void testCompleteEmpty() {
.containsExactly(expectedCandidates.toArray(String[]::new));
}
+ /** Test of {@link AbstractIdeContext#complete(CliArguments, boolean) auto-completion} for long option. */
+ @Test
+ public void testCompleteLongOptionBatch() {
+
+ // arrange
+ boolean includeContextOptions = true;
+ AbstractIdeContext context = newContext(PROJECT_BASIC, null, false);
+ CliArguments args = CliArguments.ofCompletion("--b");
+ args.next();
+ List expectedCandidates = List.of("--batch");
+ // act
+ List candidates = context.complete(args, includeContextOptions);
+ // assert
+ assertThat(candidates.stream().map(CompletionCandidate::text))
+ .containsExactly(expectedCandidates.toArray(String[]::new));
+ }
+
/** Test of {@link AbstractIdeContext#complete(CliArguments, boolean) auto-completion} for empty input. */
@Test
public void testCompleteEmptyNoCtxOptions() {
@@ -75,7 +93,7 @@ public void testCompleteShortOptsCombined() {
// act
List candidates = context.complete(args, true);
// assert
- assertThat(candidates.stream().map(CompletionCandidate::text)).containsExactly("-f", "-fb", "-fd", "-fo", "-fq",
+ assertThat(candidates.stream().map(CompletionCandidate::text)).containsExactly("-f", "-fb", "-fd", "-fh", "-fo", "-fq",
"-ft", "-fv");
}
@@ -89,7 +107,7 @@ public void testCompleteShortOptsCombinedAllButVersion() {
// act
List candidates = context.complete(args, true);
// assert
- assertThat(candidates.stream().map(CompletionCandidate::text)).containsExactly("-fbdoqt", "-fbdoqtv");
+ assertThat(candidates.stream().map(CompletionCandidate::text)).containsExactly("-fbdoqt", "-fbdoqth", "-fbdoqtv");
}
/** Test of {@link AbstractIdeContext#complete(CliArguments, boolean) auto-completion} for input "help", "". */
@@ -120,8 +138,21 @@ public void testCompleteVersionNoMoreArgs() {
assertThat(candidates).isEmpty();
}
+ /** Test of {@link AbstractIdeContext#complete(CliArguments, boolean) auto-completion} for an option inside a commandlet. */
+ @Test
+ public void testCompleteCommandletOption() {
+
+ // arrange
+ AbstractIdeContext context = newContext(PROJECT_BASIC, null, false);
+ CliArguments args = CliArguments.ofCompletion("get-version", "--c");
+ // act
+ List candidates = context.complete(args, true);
+ // assert
+ assertThat(candidates.stream().map(CompletionCandidate::text)).containsExactly("--configured");
+ }
+
private static List getExpectedCandidates(AbstractIdeContext context, boolean commandlets,
- boolean ctxOptions, boolean addVersionAlias) {
+ boolean ctxOptions, boolean addAlias) {
List expectedCandidates = new ArrayList<>();
if (ctxOptions) {
@@ -137,9 +168,14 @@ private static List getExpectedCandidates(AbstractIdeContext context, bo
if (commandlets) {
for (Commandlet cmd : context.getCommandletManager().getCommandlets()) {
expectedCandidates.add(cmd.getName());
- }
- if (addVersionAlias) {
- expectedCandidates.add("-v"); // alias for VersionCommandlet (--version)
+ if (addAlias) {
+ Property> firstProperty = cmd.getValues().get(0);
+ assert (firstProperty instanceof KeywordProperty);
+ String alias = firstProperty.getAlias();
+ if (alias != null) {
+ expectedCandidates.add(alias);
+ }
+ }
}
}
Collections.sort(expectedCandidates);
diff --git a/cli/src/test/java/com/devonfw/tools/ide/completion/IdeCompleterTest.java b/cli/src/test/java/com/devonfw/tools/ide/completion/IdeCompleterTest.java
index a979f8db4..eef53376c 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/completion/IdeCompleterTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/completion/IdeCompleterTest.java
@@ -31,6 +31,17 @@ public void testIdeCompleterVersion() {
assertBuffer("--version ", new TestBuffer("--vers").tab());
}
+ /**
+ * Test of 1st level auto-completion (commandlet name). Here we test the special case of the {@link com.devonfw.tools.ide.commandlet.VersionCommandlet} that
+ * has a long-option style.
+ */
+ @Test
+ public void testIdeCompleterBatch() {
+
+ this.reader.setCompleter(newCompleter());
+ assertBuffer("--batch ", new TestBuffer("--b").tab());
+ }
+
/**
* Test of 2nd level auto-completion with tool property of {@link com.devonfw.tools.ide.commandlet.InstallCommandlet}.
*/
@@ -92,7 +103,7 @@ public void testIdeCompleterNonExistentCommand() {
public void testIdeCompleterPreventsOptionsAfterCommandWithMinus() {
this.reader.setCompleter(newCompleter());
- assertBuffer("get-version -", new TestBuffer("get-version -").tab().tab());
+ assertBuffer("get-version --configured ", new TestBuffer("get-version -").tab().tab());
assertBuffer("get-version - ", new TestBuffer("get-version - ").tab().tab());
}
@@ -119,6 +130,26 @@ public void testIdeCompleterHandlesOptionsBeforeCommand() {
assertBuffer("get-version mvn ", new TestBuffer("get-version mv").tab().tab());
}
+ /**
+ * Test of completion of options after commandlets.
+ */
+ @Test
+ public void testIdeCompleterWithOptionAfterCommandletWorks() {
+
+ this.reader.setCompleter(newCompleter());
+ assertBuffer("env --bash ", new TestBuffer("env --ba").tab().tab());
+ }
+
+ /**
+ * Test of completion of options and arguments after commandlets.
+ */
+ @Test
+ public void testIdeCompleterWithOptionAndArguments() {
+
+ this.reader.setCompleter(newCompleter());
+ assertBuffer("get-version --configured ", new TestBuffer("get-version --c").tab().tab());
+ }
+
private IdeCompleter newCompleter() {
return new IdeCompleter(newTestContext());
diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeContextTest.java b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeContextTest.java
index 6070241b0..39a0f0016 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeContextTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeContextTest.java
@@ -122,8 +122,8 @@ private static List assertProgressEventsAn
List eventList = progressBar.getEventList();
assertThat(eventList).hasSize(chunkCount + 1);
// extra case for unknown file size (indefinite progress bar)
- if (progressBar.getMaxLength() != -1L) {
- assertThat(progressBar.getMaxLength()).isEqualTo(maxSize);
+ if (progressBar.getMaxSize() != -1L) {
+ assertThat(progressBar.getMaxSize()).isEqualTo(maxSize);
}
return eventList;
}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java
index d9d730c04..35059a819 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java
@@ -5,11 +5,16 @@
import java.util.HashMap;
import java.util.Map;
+import com.devonfw.tools.ide.commandlet.Commandlet;
+import com.devonfw.tools.ide.commandlet.CommandletManager;
+import com.devonfw.tools.ide.commandlet.TestCommandletManager;
import com.devonfw.tools.ide.common.SystemPath;
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
import com.devonfw.tools.ide.environment.EnvironmentVariables;
import com.devonfw.tools.ide.environment.EnvironmentVariablesPropertiesFile;
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
+import com.devonfw.tools.ide.environment.IdeSystem;
+import com.devonfw.tools.ide.environment.IdeSystemTestImpl;
import com.devonfw.tools.ide.io.IdeProgressBar;
import com.devonfw.tools.ide.io.IdeProgressBarTestImpl;
import com.devonfw.tools.ide.log.IdeLogger;
@@ -37,14 +42,15 @@ public class AbstractIdeTestContext extends AbstractIdeContext {
private ProcessContext mockContext;
+ private TestCommandletManager testCommandletManager;
+
/**
* The constructor.
*
* @param logger the {@link IdeLogger}.
* @param workingDirectory the optional {@link Path} to current working directory.
- * @param answers the automatic answers simulating a user in test.
*/
- public AbstractIdeTestContext(IdeStartContextImpl logger, Path workingDirectory, String... answers) {
+ public AbstractIdeTestContext(IdeStartContextImpl logger, Path workingDirectory) {
super(logger, workingDirectory);
this.answers = new String[0];
@@ -83,6 +89,10 @@ protected String readLine() {
return this.answers[this.answerIndex++];
}
+ /**
+ * @param answers the answers for interactive questions in order (e.g. if "yes" is given as first answer, this will be used to answer the first
+ * question).
+ */
public void setAnswers(String... answers) {
requireMutable();
this.answers = answers;
@@ -98,14 +108,12 @@ public Map getProgressBarMap() {
}
@Override
- public IdeProgressBar prepareProgressBar(String taskName, long size) {
+ public IdeProgressBar newProgressBar(String title, long maxSize, String unitName, long unitSize) {
- IdeProgressBarTestImpl progressBar = new IdeProgressBarTestImpl(taskName, size);
- IdeProgressBarTestImpl duplicate = this.progressBarMap.put(taskName, progressBar);
+ IdeProgressBarTestImpl progressBar = new IdeProgressBarTestImpl(title, maxSize, unitName, unitSize);
+ IdeProgressBarTestImpl duplicate = this.progressBarMap.put(title, progressBar);
// If we have multiple downloads or unpacking, we may have an existing "Downloading" or "Unpacking" key
- if ((!taskName.equals("Downloading")) && (!taskName.equals("Unpacking"))) {
- assert duplicate == null;
- }
+ assert (title.equals(IdeProgressBar.TITLE_DOWNLOADING)) || (title.equals(IdeProgressBar.TITLE_EXTRACTING)) || duplicate == null;
return progressBar;
}
@@ -122,6 +130,23 @@ protected AbstractEnvironmentVariables createSystemVariables() {
return super.createSystemVariables();
}
+ @Override
+ public IdeSystemTestImpl getSystem() {
+
+ if (this.system == null) {
+ this.system = new IdeSystemTestImpl(this);
+ }
+ return (IdeSystemTestImpl) this.system;
+ }
+
+ /**
+ * @param system the new value of {@link #getSystem()}.
+ */
+ public void setSystem(IdeSystem system) {
+
+ this.system = system;
+ }
+
@Override
protected SystemPath computeSystemPath() {
@@ -199,4 +224,44 @@ public void setDefaultToolRepository(ToolRepository defaultToolRepository) {
this.defaultToolRepository = defaultToolRepository;
}
+
+ /**
+ * @param settingsPath the new value of {@link #getSettingsPath()}.
+ */
+ public void setSettingsPath(Path settingsPath) {
+
+ this.settingsPath = settingsPath;
+ }
+
+ /**
+ * @param pluginsPath the new value of {@link #getPluginsPath()}.
+ */
+ public void setPluginsPath(Path pluginsPath) {
+
+ this.pluginsPath = pluginsPath;
+ }
+
+ /**
+ * @param commandletManager the new value of {@link #getCommandletManager()}.
+ */
+ public void setCommandletManager(CommandletManager commandletManager) {
+ if (commandletManager instanceof TestCommandletManager tcm) {
+ this.testCommandletManager = tcm;
+ } else {
+ this.testCommandletManager = null;
+ }
+ this.commandletManager = commandletManager;
+ }
+
+ /**
+ * @param commandlet the {@link Commandlet} to add to {@link #getCommandletManager()} for testing.
+ */
+ public void addCommandlet(Commandlet commandlet) {
+
+ if (this.testCommandletManager == null) {
+ setCommandletManager(new TestCommandletManager(this));
+ }
+ this.testCommandletManager.add(commandlet);
+ }
+
}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/GitContextMock.java b/cli/src/test/java/com/devonfw/tools/ide/context/GitContextMock.java
deleted file mode 100644
index cb5cdcca8..000000000
--- a/cli/src/test/java/com/devonfw/tools/ide/context/GitContextMock.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package com.devonfw.tools.ide.context;
-
-import java.nio.file.Path;
-
-/**
- * Mock implementation of {@link GitContext}.
- */
-public class GitContextMock implements GitContext {
-
- private static final String MOCKED_URL_VALUE = "mocked url value";
-
- @Override
- public void pullOrCloneIfNeeded(String repoUrl, String branch, Path targetRepository) {
-
- }
-
- @Override
- public void pullOrCloneAndResetIfNeeded(String repoUrl, Path targetRepository, String branch, String remoteName) {
-
- }
-
- @Override
- public void pullOrClone(String gitRepoUrl, Path targetRepository) {
-
- }
-
- @Override
- public void pullOrClone(String gitRepoUrl, Path targetRepository, String branch) {
-
- }
-
- @Override
- public void clone(GitUrl gitRepoUrl, Path targetRepository) {
-
- }
-
- @Override
- public void pull(Path targetRepository) {
-
- }
-
- @Override
- public void fetch(Path targetRepository, String remote, String branch) {
-
- }
-
- @Override
- public void reset(Path targetRepository, String branchName, String remoteName) {
-
- }
-
- @Override
- public void cleanup(Path targetRepository) {
-
- }
-
- @Override
- public String retrieveGitUrl(Path repository) {
-
- return MOCKED_URL_VALUE;
- }
-
- @Override
- public boolean fetchIfNeeded(Path targetRepository, String remoteName, String branch) {
-
- return false;
- }
-
- @Override
- public boolean fetchIfNeeded(Path targetRepository) {
-
- return false;
- }
-
- @Override
- public boolean isRepositoryUpdateAvailable(Path targetRepository) {
-
- return false;
- }
-
- @Override
- public String determineCurrentBranch(Path repository) {
-
- return "main";
- }
-
- @Override
- public String determineRemote(Path repository) {
-
- return "origin";
- }
-}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/IdeTestContext.java b/cli/src/test/java/com/devonfw/tools/ide/context/IdeTestContext.java
index 2e5040a1c..c56dd7fdc 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/context/IdeTestContext.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/context/IdeTestContext.java
@@ -2,6 +2,8 @@
import java.nio.file.Path;
+import com.devonfw.tools.ide.git.GitContext;
+import com.devonfw.tools.ide.git.GitContextMock;
import com.devonfw.tools.ide.log.IdeLogLevel;
import com.devonfw.tools.ide.log.IdeTestLogger;
import com.devonfw.tools.ide.process.ProcessContext;
diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/ProcessContextGitMock.java b/cli/src/test/java/com/devonfw/tools/ide/context/ProcessContextGitMock.java
index 6c3fac919..5c891d257 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/context/ProcessContextGitMock.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/context/ProcessContextGitMock.java
@@ -121,6 +121,11 @@ public ProcessContext withPathEntry(Path path) {
@Override
public ProcessResult run(ProcessMode processMode) {
+ StringBuilder command = new StringBuilder("git");
+ for (String arg : this.arguments) {
+ command.append(' ');
+ command.append(arg);
+ }
Path gitFolderPath = this.directory.resolve(".git");
// deletes a newly added folder
if (this.arguments.contains("clean")) {
@@ -176,7 +181,7 @@ public ProcessResult run(ProcessMode processMode) {
}
}
this.arguments.clear();
- return new ProcessResultImpl(this.exitCode, this.outs, this.errors);
+ return new ProcessResultImpl("git", command.toString(), this.exitCode, this.outs, this.errors);
}
}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/environment/IdeSystemTestImpl.java b/cli/src/test/java/com/devonfw/tools/ide/environment/IdeSystemTestImpl.java
new file mode 100644
index 000000000..0486ce956
--- /dev/null
+++ b/cli/src/test/java/com/devonfw/tools/ide/environment/IdeSystemTestImpl.java
@@ -0,0 +1,59 @@
+package com.devonfw.tools.ide.environment;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import com.devonfw.tools.ide.log.IdeLogger;
+
+/**
+ * Extends {@link IdeSystemImpl} for testing. It will not modify your {@link System} and allows to modify environment variables for testing.
+ */
+public class IdeSystemTestImpl extends IdeSystemImpl {
+
+ /**
+ * @param logger the {@link IdeLogger}.
+ */
+ public IdeSystemTestImpl(IdeLogger logger) {
+
+ this(logger, new Properties(), new HashMap<>());
+ this.environmentVariables.put("PATH", System.getenv("PATH"));
+ }
+
+ /**
+ * @param logger the {@link IdeLogger}.
+ * @param systemProperties the {@link System#getProperties() system properties} for testing.
+ * @param environmentVariables the {@link System#getenv() environment variables} for testing.
+ */
+ public IdeSystemTestImpl(IdeLogger logger, Properties systemProperties,
+ Map environmentVariables) {
+
+ super(logger, systemProperties, environmentVariables);
+ }
+
+ /**
+ * @param key the name of the environment variable to mock.
+ * @param value the value of the environment variable to mock.
+ */
+ public void setEnv(String key, String value) {
+
+ this.environmentVariables.put(key, value);
+ }
+
+ /**
+ * @return the internal system {@link Properties}.
+ */
+ public Properties getProperties() {
+
+ return this.systemProperties;
+ }
+
+ /**
+ * @param logger the {@link IdeLogger}.
+ * @return a new instance of {@link IdeSystemTestImpl} initialized with {@link System} values but decoupled so changes do not affect {@link System}.
+ */
+ public static IdeSystemTestImpl ofSystemDefaults(IdeLogger logger) {
+
+ return new IdeSystemTestImpl(logger, new Properties(System.getProperties()), new HashMap<>(System.getenv()));
+ }
+}
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
new file mode 100644
index 000000000..014655d9b
--- /dev/null
+++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java
@@ -0,0 +1,87 @@
+package com.devonfw.tools.ide.git;
+
+import java.nio.file.Path;
+
+/**
+ * Mock implementation of {@link GitContext}.
+ */
+public class GitContextMock implements GitContext {
+
+ private static final String MOCKED_URL_VALUE = "mocked url value";
+
+ @Override
+ public void pullOrCloneIfNeeded(GitUrl gitUrl, Path repository) {
+
+ }
+
+ @Override
+ public void pullOrCloneAndResetIfNeeded(GitUrl gitUrl, Path repository, String remoteName) {
+
+ }
+
+ @Override
+ public void pullOrClone(GitUrl gitUrl, Path repository) {
+
+ }
+
+ @Override
+ public void clone(GitUrl gitUrl, Path repository) {
+
+ }
+
+ @Override
+ public void pull(Path repository) {
+
+ }
+
+ @Override
+ public void fetch(Path repository, String remote, String branch) {
+
+ }
+
+ @Override
+ public void reset(Path repository, String branchName, String remoteName) {
+
+ }
+
+ @Override
+ public void cleanup(Path repository) {
+
+ }
+
+ @Override
+ public String retrieveGitUrl(Path repository) {
+
+ return MOCKED_URL_VALUE;
+ }
+
+ @Override
+ public boolean fetchIfNeeded(Path repository, String remoteName, String branch) {
+
+ return false;
+ }
+
+ @Override
+ public boolean fetchIfNeeded(Path repository) {
+
+ return false;
+ }
+
+ @Override
+ public boolean isRepositoryUpdateAvailable(Path repository) {
+
+ return false;
+ }
+
+ @Override
+ public String determineCurrentBranch(Path repository) {
+
+ return "main";
+ }
+
+ @Override
+ public String determineRemote(Path repository) {
+
+ return "origin";
+ }
+}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/GitContextTest.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextTest.java
similarity index 88%
rename from cli/src/test/java/com/devonfw/tools/ide/context/GitContextTest.java
rename to cli/src/test/java/com/devonfw/tools/ide/git/GitContextTest.java
index b7d1a0ee9..9d56664a1 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/context/GitContextTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextTest.java
@@ -1,4 +1,4 @@
-package com.devonfw.tools.ide.context;
+package com.devonfw.tools.ide.git;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -13,6 +13,10 @@
import org.junit.jupiter.api.io.TempDir;
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.io.FileAccess;
import com.devonfw.tools.ide.io.FileAccessImpl;
@@ -44,16 +48,15 @@ public void testRunGitCloneInOfflineModeThrowsException(@TempDir Path tempDir) {
// arrange
String gitRepoUrl = "https://github.com/test";
IdeTestContext context = newGitContext(tempDir);
- this.processContext.getOuts().add("test-remote");
- context.setOnline(Boolean.FALSE);
+ GitUrl gitUrl = new GitUrl(gitRepoUrl, "branch");
+ context.getStartContext().setOfflineMode(true);
- //IdeContext context = newGitContext(tempDir, errors, outs, 0, false);
// act
CliException e1 = assertThrows(CliException.class, () -> {
- context.getGitContext().pullOrClone(gitRepoUrl, tempDir, "");
+ context.getGitContext().pullOrClone(gitUrl, tempDir);
});
// assert
- assertThat(e1).hasMessageContaining(gitRepoUrl).hasMessageContaining(tempDir.toString())
+ assertThat(e1).hasMessageContaining(gitRepoUrl).hasMessage("You are offline but Internet access is required for git clone of " + gitUrl)
.hasMessageContaining("offline");
}
@@ -70,7 +73,7 @@ public void testRunGitClone(@TempDir Path tempDir) {
IdeTestContext context = newGitContext(tempDir);
this.processContext.getOuts().add("test-remote");
// act
- context.getGitContext().pullOrClone(gitRepoUrl, tempDir);
+ context.getGitContext().pullOrClone(GitUrl.of(gitRepoUrl), tempDir);
// assert
assertThat(tempDir.resolve(".git").resolve("url")).hasContent(gitRepoUrl);
}
@@ -91,7 +94,7 @@ public void testRunGitPullWithoutForce(@TempDir Path tempDir) {
Path gitFolderPath = tempDir.resolve(".git");
fileAccess.mkdirs(gitFolderPath);
// act
- context.getGitContext().pullOrClone(gitRepoUrl, tempDir);
+ context.getGitContext().pullOrClone(GitUrl.of(gitRepoUrl), tempDir);
// assert
assertThat(tempDir.resolve(".git").resolve("update")).hasContent(this.processContext.getNow().toString());
}
@@ -128,7 +131,7 @@ public void testRunGitPullWithForceStartsReset(@TempDir Path tempDir) {
IdeTestContext context = newGitContext(tempDir);
this.processContext.getOuts().add("test-remote");
// act
- context.getGitContext().pullOrCloneAndResetIfNeeded(gitRepoUrl, tempDir, "master", "origin");
+ context.getGitContext().pullOrCloneAndResetIfNeeded(new GitUrl(gitRepoUrl, "master"), tempDir, "origin");
// assert
assertThat(modifiedFile).hasContent("original");
}
@@ -156,7 +159,7 @@ public void testRunGitPullWithForceStartsCleanup(@TempDir Path tempDir) {
throw new RuntimeException(e);
}
// act
- gitContext.pullOrCloneAndResetIfNeeded(gitRepoUrl, tempDir, "master", "origin");
+ gitContext.pullOrCloneAndResetIfNeeded(GitUrl.ofMain(gitRepoUrl), tempDir, "origin");
// assert
assertThat(tempDir.resolve("new-folder")).doesNotExist();
}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/GitOperationTest.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitOperationTest.java
similarity index 85%
rename from cli/src/test/java/com/devonfw/tools/ide/context/GitOperationTest.java
rename to cli/src/test/java/com/devonfw/tools/ide/git/GitOperationTest.java
index ec2f29890..e194541e7 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/context/GitOperationTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitOperationTest.java
@@ -1,4 +1,4 @@
-package com.devonfw.tools.ide.context;
+package com.devonfw.tools.ide.git;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -8,6 +8,9 @@
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mockito;
+import com.devonfw.tools.ide.context.AbstractIdeContextTest;
+import com.devonfw.tools.ide.context.IdeTestContext;
+
/**
* Test of {@link GitOperation}.
*/
@@ -16,6 +19,7 @@ public class GitOperationTest extends AbstractIdeContextTest {
private static final String URL = "https://github.com/devonfw/IDEasy.git";
private static final String REMOTE = "origin";
private static final String BRANCH = "main";
+ private static final GitUrl GIT_URL = new GitUrl(URL, BRANCH);
@Test
public void testFetchSkippedIfTimestampFileUpToDate(@TempDir Path tempDir) throws Exception {
@@ -28,7 +32,7 @@ public void testFetchSkippedIfTimestampFileUpToDate(@TempDir Path tempDir) throw
Path repo = createFakeGitRepo(tempDir, operation.getTimestampFilename());
// act
- operation.executeIfNeeded(context, null, repo, REMOTE, BRANCH);
+ operation.executeIfNeeded(context, GIT_URL, repo, REMOTE);
// assert
Mockito.verifyNoInteractions(mock);
@@ -45,7 +49,7 @@ public void testFetchCalledIfTimestampFileNotPresent(@TempDir Path tempDir) thro
Path repo = createFakeGitRepo(tempDir, null);
// act
- operation.executeIfNeeded(context, null, repo, REMOTE, BRANCH);
+ operation.executeIfNeeded(context, GIT_URL, repo, REMOTE);
// assert
Mockito.verify(mock).fetch(repo, REMOTE, BRANCH);
@@ -62,7 +66,7 @@ public void testFetchCalledIfTimestampFileOutdated(@TempDir Path tempDir) throws
Path repo = createFakeGitRepo(tempDir, operation.getTimestampFilename(), true);
// act
- operation.executeIfNeeded(context, null, repo, REMOTE, BRANCH);
+ operation.executeIfNeeded(context, GIT_URL, repo, REMOTE);
// assert
Mockito.verify(mock).fetch(repo, REMOTE, BRANCH);
@@ -80,7 +84,7 @@ public void testFetchCalledIfTimestampFileUpToDateButForceMode(@TempDir Path tem
Path repo = createFakeGitRepo(tempDir, operation.getTimestampFilename(), true);
// act
- operation.executeIfNeeded(context, null, repo, REMOTE, BRANCH);
+ operation.executeIfNeeded(context, GIT_URL, repo, REMOTE);
// assert
Mockito.verify(mock).fetch(repo, REMOTE, BRANCH);
@@ -98,7 +102,7 @@ public void testFetchSkippedIfTimestampFileNotPresentButOfflineMode(@TempDir Pat
Path repo = createFakeGitRepo(tempDir, null);
// act
- operation.executeIfNeeded(context, null, repo, REMOTE, BRANCH);
+ operation.executeIfNeeded(context, GIT_URL, repo, REMOTE);
// assert
Mockito.verifyNoInteractions(mock);
@@ -115,7 +119,7 @@ public void testPullOrCloneSkippedIfTimestampFileUpToDate(@TempDir Path tempDir)
Path repo = createFakeGitRepo(tempDir, operation.getTimestampFilename());
// act
- operation.executeIfNeeded(context, URL, repo, null, BRANCH);
+ operation.executeIfNeeded(context, GIT_URL, repo, null);
// assert
Mockito.verifyNoInteractions(mock);
@@ -132,10 +136,10 @@ public void testPullOrCloneCalledIfTimestampFileNotPresent(@TempDir Path tempDir
Path repo = createFakeGitRepo(tempDir, null);
// act
- operation.executeIfNeeded(context, URL, repo, null, BRANCH);
+ operation.executeIfNeeded(context, GIT_URL, repo, null);
// assert
- Mockito.verify(mock).pullOrClone(URL, repo, BRANCH);
+ Mockito.verify(mock).pullOrClone(GIT_URL, repo);
}
@Test
@@ -149,10 +153,10 @@ public void testPullOrCloneCalledIfTimestampFileOutdated(@TempDir Path tempDir)
Path repo = createFakeGitRepo(tempDir, operation.getTimestampFilename(), true);
// act
- operation.executeIfNeeded(context, URL, repo, null, BRANCH);
+ operation.executeIfNeeded(context, GIT_URL, repo, null);
// assert
- Mockito.verify(mock).pullOrClone(URL, repo, BRANCH);
+ Mockito.verify(mock).pullOrClone(GIT_URL, repo);
}
@Test
@@ -167,10 +171,10 @@ public void testPullOrCloneCalledIfTimestampFileUpToDateButForceMode(@TempDir Pa
Path repo = createFakeGitRepo(tempDir, operation.getTimestampFilename(), true);
// act
- operation.executeIfNeeded(context, URL, repo, null, BRANCH);
+ operation.executeIfNeeded(context, GIT_URL, repo, null);
// assert
- Mockito.verify(mock).pullOrClone(URL, repo, BRANCH);
+ Mockito.verify(mock).pullOrClone(GIT_URL, repo);
}
@Test
@@ -185,7 +189,7 @@ public void testPullOrCloneSkippedIfTimestampFileNotPresentButOfflineMode(@TempD
Path repo = createFakeGitRepo(tempDir, null);
// act
- operation.executeIfNeeded(context, URL, repo, null, BRANCH);
+ operation.executeIfNeeded(context, GIT_URL, repo, null);
// assert
Mockito.verifyNoInteractions(mock);
@@ -204,10 +208,10 @@ public void testPullOrCloneSkippedIfRepoNotInitializedAndOfflineMode(@TempDir Pa
Files.createDirectories(repo);
// act
- operation.executeIfNeeded(context, URL, repo, null, BRANCH);
+ operation.executeIfNeeded(context, GIT_URL, repo, null);
// assert
- Mockito.verify(mock).pullOrClone(URL, repo, BRANCH);
+ Mockito.verify(mock).pullOrClone(GIT_URL, repo);
}
private Path createFakeGitRepo(Path dir, String file) throws Exception {
diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/GitUrlSyntaxTest.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitUrlSyntaxTest.java
similarity index 94%
rename from cli/src/test/java/com/devonfw/tools/ide/context/GitUrlSyntaxTest.java
rename to cli/src/test/java/com/devonfw/tools/ide/git/GitUrlSyntaxTest.java
index 4687f62bd..b8c3e8025 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/context/GitUrlSyntaxTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitUrlSyntaxTest.java
@@ -1,4 +1,7 @@
-package com.devonfw.tools.ide.context;
+package com.devonfw.tools.ide.git;
+
+import com.devonfw.tools.ide.context.AbstractIdeContextTest;
+import com.devonfw.tools.ide.context.IdeTestContext;
import org.junit.jupiter.api.Test;
diff --git a/cli/src/test/java/com/devonfw/tools/ide/git/GitUrlTest.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitUrlTest.java
new file mode 100644
index 000000000..450fd7a9e
--- /dev/null
+++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitUrlTest.java
@@ -0,0 +1,54 @@
+package com.devonfw.tools.ide.git;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test of {@link GitUrl}.
+ */
+public class GitUrlTest extends Assertions {
+
+ /** Test {@link GitUrl#of(String)} with url having branch. */
+ @Test
+ public void testOfUrlWithBranch() {
+
+ // arrange
+ String url = "https://github.com/devonfw/IDEasy.git";
+ String branch = "feature/xyz";
+ String urlWithBranch = url + "#" + branch;
+ // act
+ GitUrl gitUrl = GitUrl.of(urlWithBranch);
+ // assert
+ assertThat(gitUrl.url()).isEqualTo(url);
+ assertThat(gitUrl.branch()).isEqualTo(branch);
+ assertThat(gitUrl).hasToString(urlWithBranch);
+ }
+
+ /** Test {@link GitUrl#of(String)} with url having no branch. */
+ @Test
+ public void testOfUrlWithoutBranch() {
+
+ // arrange
+ String url = "https://github.com/devonfw/IDEasy.git";
+ // act
+ GitUrl gitUrl = GitUrl.of(url);
+ // assert
+ assertThat(gitUrl.url()).isEqualTo(url);
+ assertThat(gitUrl.branch()).isNull();
+ assertThat(gitUrl).hasToString(url);
+ }
+
+ /** Test {@link GitUrl#GitUrl(String, String)} with invalid URL. */
+ @Test
+ public void testInvalidUrl() {
+
+ // arrange
+ String url = "invalid#url";
+ String branch = null;
+ // act
+ assertThatThrownBy(() -> {
+ new GitUrl(url, branch);
+ }).isInstanceOf(AssertionError.class).hasMessage("Invalid git URL " + url);
+ }
+
+}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java b/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java
index e8724d6fc..cf5fd149e 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java
@@ -478,7 +478,7 @@ public void testUnzip(@TempDir Path tempDir) {
// act
context.getFileAccess()
- .extractZip(Path.of("src/test/resources/com/devonfw/tools/ide/io").resolve("executable_and_non_executable.zip"),
+ .extractZip(Path.of("src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.zip"),
tempDir);
// assert
diff --git a/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarConsoleTest.java b/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarConsoleTest.java
index 206d6935f..598137a96 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarConsoleTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarConsoleTest.java
@@ -17,8 +17,7 @@ public class IdeProgressBarConsoleTest extends AbstractIdeContextTest {
private IdeProgressBarConsole newProgressBar(long maxSize) {
SystemInfo systemInfo = SystemInfoImpl.INSTANCE;
- IdeProgressBarConsole progressBarConsole = new IdeProgressBarConsole(systemInfo, "downloading", maxSize);
- return progressBarConsole;
+ return new IdeProgressBarConsole(systemInfo, IdeProgressBar.TITLE_DOWNLOADING, maxSize, "MB", 1_000_000);
}
@Test
@@ -67,7 +66,7 @@ public void testProgressBarMaxSizeKnownStepBy() throws Exception {
// assert
assertThat(progressBarConsole.getProgressBar().isIndefinite()).isEqualTo(false);
- assertThat(progressBarConsole.getMaxLength()).isEqualTo(maxSize);
+ assertThat(progressBarConsole.getMaxSize()).isEqualTo(maxSize);
assertThat(progressBarConsole.getCurrentProgress()).isEqualTo(maxSize);
}
@@ -83,7 +82,7 @@ public void testProgressBarMaxSizeKnownDoStepTo() throws Exception {
// assert
assertThat(progressBarConsole.getProgressBar().isIndefinite()).isEqualTo(false);
- assertThat(progressBarConsole.getMaxLength()).isEqualTo(maxSize);
+ assertThat(progressBarConsole.getMaxSize()).isEqualTo(maxSize);
assertThat(progressBarConsole.getCurrentProgress()).isEqualTo(maxSize);
}
@@ -100,7 +99,7 @@ public void testProgressBarMaxSizeKnownIncompleteSteps() throws Exception {
// act
progressBarConsole.close();
// assert
- assertThat(progressBarConsole.getMaxLength()).isEqualTo(maxSize);
+ assertThat(progressBarConsole.getMaxSize()).isEqualTo(maxSize);
}
@Test
diff --git a/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTest.java b/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTest.java
index ed424386f..d7c440fa5 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTest.java
@@ -68,17 +68,12 @@ public void testProgressBarDownloadWithMissingContentLength(@TempDir Path tempDi
//assert
assertUnknownProgressBar(context, "Downloading", MAX_LENGTH);
- checkLogMessageForMissingContentLength(context, testUrl);
+ assertThat(context).logAtWarning().hasMessage(
+ "Content-Length was not provided by download from " + testUrl);
assertThat(tempDir.resolve("windows_x64_url.tgz")).exists();
IdeProgressBarTestImpl progressBar = context.getProgressBarMap().get(taskName);
- assertThat(progressBar.getMaxLength()).isEqualTo(-1);
- }
-
- private void checkLogMessageForMissingContentLength(IdeTestContext context, String source) {
-
- assertThat(context).logAtWarning().hasMessage(
- "Content-Length was not provided by download/copy source: " + source + ".");
+ assertThat(progressBar.getMaxSize()).isEqualTo(-1);
}
/**
@@ -108,6 +103,6 @@ public void testProgressBarCopyWithKnownFileSize(@TempDir Path tempDir) {
assertProgressBar(context, "Copying", maxSize);
assertThat(tempDir.resolve("windows_x64_url.tgz")).exists();
IdeProgressBarTestImpl progressBar = context.getProgressBarMap().get(taskName);
- assertThat(progressBar.getMaxLength()).isEqualTo(maxSize);
+ assertThat(progressBar.getMaxSize()).isEqualTo(maxSize);
}
}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTestImpl.java b/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTestImpl.java
index 64ca4c286..89b24892d 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTestImpl.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/io/IdeProgressBarTestImpl.java
@@ -15,9 +15,6 @@ public class IdeProgressBarTestImpl extends AbstractIdeProgressBar {
/** Ending time of an {@link IdeProgressBar}. */
private Instant end;
- /** The task name of an {@link IdeProgressBar}. */
- private final String name;
-
/** The total span of an {@link IdeProgressBar}. */
private long total;
@@ -27,13 +24,14 @@ public class IdeProgressBarTestImpl extends AbstractIdeProgressBar {
/**
* The constructor.
*
- * @param name the task name.
- * @param max maximum length of the bar.
+ * @param title the {@link #getTitle() title}.
+ * @param maxSize the {@link #getMaxSize() maximum size}.
+ * @param unitName the {@link #getUnitName() unit name}.
+ * @param unitSize the {@link #getUnitSize() unit size}.
*/
- public IdeProgressBarTestImpl(String name, long max) {
- super(max);
+ public IdeProgressBarTestImpl(String title, long maxSize, String unitName, long unitSize) {
+ super(title, maxSize, unitName, unitSize);
this.start = Instant.now();
- this.name = name;
this.eventList = new ArrayList<>();
}
@@ -56,8 +54,8 @@ public void close() {
this.end = Instant.now();
}
- if (getMaxLength() != -1) {
- assert this.total == getMaxLength();
+ if (getMaxSize() != -1) {
+ assert this.total == getMaxSize();
}
}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/log/IdeLogEntry.java b/cli/src/test/java/com/devonfw/tools/ide/log/IdeLogEntry.java
deleted file mode 100644
index c2a840a00..000000000
--- a/cli/src/test/java/com/devonfw/tools/ide/log/IdeLogEntry.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package com.devonfw.tools.ide.log;
-
-/**
- * Single entry that was logged by {@link IdeSubLoggerTest}.
- *
- * @param level the {@link IdeLogLevel}.
- * @param message the message that has been logged.
- */
-public record IdeLogEntry(IdeLogLevel level, String message, boolean contains) {
-
- public IdeLogEntry(IdeLogLevel level, String message) {
- this(level, message, false);
- }
-
- public boolean matches(IdeLogEntry entry) {
-
- if (this.level != entry.level) {
- return false;
- } else if (this.contains) {
- if (!entry.message.contains(this.message)) {
- return false;
- }
- } else if (!entry.message.equals(this.message)) {
- return false;
- }
- return true;
- }
-
- public static IdeLogEntry ofError(String message) {
-
- return new IdeLogEntry(IdeLogLevel.ERROR, message);
- }
-
- public static IdeLogEntry ofWarning(String message) {
-
- return new IdeLogEntry(IdeLogLevel.WARNING, message);
- }
-
- public static IdeLogEntry ofInfo(String message) {
-
- return new IdeLogEntry(IdeLogLevel.INFO, message);
- }
-
- public static IdeLogEntry ofStep(String message) {
-
- return new IdeLogEntry(IdeLogLevel.STEP, message);
- }
-
- public static IdeLogEntry ofSuccess(String message) {
-
- return new IdeLogEntry(IdeLogLevel.SUCCESS, message);
- }
-
- public static IdeLogEntry ofDebug(String message) {
-
- return new IdeLogEntry(IdeLogLevel.DEBUG, message);
- }
-
- public static IdeLogEntry ofTrace(String message) {
-
- return new IdeLogEntry(IdeLogLevel.TRACE, message);
- }
-
-}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/log/IdeSlf4jRootLogger.java b/cli/src/test/java/com/devonfw/tools/ide/log/IdeSlf4jRootLogger.java
index 20dfe3609..5aa4e1733 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/log/IdeSlf4jRootLogger.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/log/IdeSlf4jRootLogger.java
@@ -20,7 +20,7 @@ public IdeSlf4jRootLogger() {
super();
this.loggers = new HashMap<>();
for (IdeLogLevel level : IdeLogLevel.values()) {
- this.loggers.put(level, new IdeSubLoggerSlf4j(level));
+ this.loggers.put(level, new IdeSubLoggerSlf4j(level, null));
}
}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/log/IdeSubLoggerSlf4j.java b/cli/src/test/java/com/devonfw/tools/ide/log/IdeSubLoggerSlf4j.java
index 671d59cc2..e39bb3f28 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/log/IdeSubLoggerSlf4j.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/log/IdeSubLoggerSlf4j.java
@@ -20,57 +20,41 @@ public class IdeSubLoggerSlf4j extends AbstractIdeSubLogger {
* @param level the {@link #getLevel() log-level}.
*/
public IdeSubLoggerSlf4j(IdeLogLevel level) {
+ this(level, null);
+ }
- super(level);
+ /**
+ * The constructor.
+ *
+ * @param level the {@link #getLevel() log-level}.
+ */
+ public IdeSubLoggerSlf4j(IdeLogLevel level, IdeLogListener listener) {
+
+ super(level, false, IdeLogExceptionDetails.NONE, listener);
this.logLevel = switch (level) {
case TRACE -> Level.TRACE;
case DEBUG -> Level.DEBUG;
- case INFO, STEP, INTERACTION, SUCCESS -> Level.INFO;
case WARNING -> Level.WARN;
case ERROR -> Level.ERROR;
- default -> throw new IllegalArgumentException("" + level);
+ default -> Level.INFO;
};
}
@Override
- public String log(Throwable error, String message, Object... args) {
+ protected void doLog(String message, Throwable error) {
- if ((message == null) && (error != null)) {
- message = error.getMessage();
- if (message == null) {
- message = error.toString();
- }
- }
- String msg = message;
- if ((this.level == IdeLogLevel.STEP) || (this.level == IdeLogLevel.INTERACTION) || (this.level == IdeLogLevel.SUCCESS)) {
- msg = this.level.name() + ":" + message;
- }
LoggingEventBuilder builder = LOG.atLevel(this.logLevel);
+ String msg = message;
if (error != null) {
- builder.setCause(error);
- }
- if (args == null) {
- builder.log(msg);
- } else {
- builder.log(msg, args);
- }
- if (message == null) {
- if (error == null) {
- return null;
- } else {
- return error.toString();
+ builder = builder.setCause(error);
+ if (msg == null) {
+ msg = error.toString();
}
- } else if (args == null) {
- return message;
- } else {
- return compose(message, args);
}
- }
-
- @Override
- public boolean isEnabled() {
-
- return LOG.isEnabledForLevel(this.logLevel);
+ if (this.level.isCustom()) {
+ msg = this.level.name() + ":" + message;
+ }
+ builder.log(msg);
}
}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/log/IdeSubLoggerTest.java b/cli/src/test/java/com/devonfw/tools/ide/log/IdeSubLoggerTest.java
deleted file mode 100644
index 0b7fafa06..000000000
--- a/cli/src/test/java/com/devonfw/tools/ide/log/IdeSubLoggerTest.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.devonfw.tools.ide.log;
-
-import java.util.List;
-
-/**
- * Implementation of {@link IdeSubLogger} for testing that collects all messages and allows to check if an expected message was logged.
- */
-public class IdeSubLoggerTest extends IdeSubLoggerSlf4j {
-
- private final List entries;
-
- /**
- * The constructor.
- *
- * @param level the {@link #getLevel() log-level}.
- */
- public IdeSubLoggerTest(IdeLogLevel level, List entries) {
-
- super(level);
- this.entries = entries;
- }
-
- @Override
- public String log(Throwable error, String message, Object... args) {
-
- String result = super.log(error, message, args);
- this.entries.add(new IdeLogEntry(level, result));
- return result;
- }
-
- /**
- * @return the {@link List} of {@link IdeLogEntry} that have been logged for test assertions.
- */
- public List getEntries() {
-
- return this.entries;
- }
-
- @Override
- public boolean isEnabled() {
-
- return true;
- }
-
-}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/log/IdeTestLogger.java b/cli/src/test/java/com/devonfw/tools/ide/log/IdeTestLogger.java
index c51cc7565..ca889bbc2 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/log/IdeTestLogger.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/log/IdeTestLogger.java
@@ -10,7 +10,7 @@
*/
public class IdeTestLogger extends IdeStartContextImpl {
- private final List entries;
+ private final IdeLogListenerCollector collector;
public IdeTestLogger() {
@@ -19,13 +19,13 @@ public IdeTestLogger() {
public IdeTestLogger(IdeLogLevel minLogLevel) {
- this(new ArrayList<>(), minLogLevel);
+ this(new ArrayList<>(), minLogLevel, new IdeLogListenerCollector());
}
- private IdeTestLogger(List entries, IdeLogLevel minLogLevel) {
+ private IdeTestLogger(List entries, IdeLogLevel minLogLevel, IdeLogListenerCollector collector) {
- super(minLogLevel, level -> new IdeSubLoggerTest(level, entries));
- this.entries = entries;
+ super(minLogLevel, level -> new IdeSubLoggerSlf4j(level, collector));
+ this.collector = collector;
}
/**
@@ -33,6 +33,6 @@ private IdeTestLogger(List entries, IdeLogLevel minLogLevel) {
*/
public List getEntries() {
- return this.entries;
+ return this.collector.getEntries();
}
}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/network/NetworkProxyTest.java b/cli/src/test/java/com/devonfw/tools/ide/network/NetworkProxyTest.java
new file mode 100644
index 000000000..dfa7f531b
--- /dev/null
+++ b/cli/src/test/java/com/devonfw/tools/ide/network/NetworkProxyTest.java
@@ -0,0 +1,178 @@
+package com.devonfw.tools.ide.network;
+
+import org.junit.jupiter.api.Test;
+
+import com.devonfw.tools.ide.context.AbstractIdeContextTest;
+import com.devonfw.tools.ide.context.IdeTestContext;
+
+/**
+ * Test of {@link NetworkProxy}.
+ */
+public class NetworkProxyTest extends AbstractIdeContextTest {
+
+ private static final String PROXY_DOCUMENTATION_PAGE = "https://github.com/devonfw/IDEasy/blob/main/documentation/proxy-support.adoc";
+
+ /**
+ * Verifies that when the HTTP_PROXY variable contains a malformed URL, {@link NetworkProxy#configure()} does not configure proxy properties.
+ */
+ @Test
+ public void testHttpProxyMalformedUrl() {
+
+ // arrange
+ IdeTestContext context = new IdeTestContext();
+ NetworkProxy networkProxy = new NetworkProxy(context);
+ String invalidUrl = "htt:p//example.com";
+ context.getSystem().setEnv("HTTP_PROXY", invalidUrl);
+ context.getSystem().setEnv("no_proxy", ".foo.com,localhost");
+
+ // act
+ networkProxy.configure();
+
+ // assert
+ assertThat(context.getSystem().getProperties()).isEmpty();
+ assertThat(context).logAtWarning().hasMessageContaining("Invalid http proxy configuration detected with URL " + invalidUrl + ".");
+ assertThat(context).logAtWarning().hasMessageContaining(PROXY_DOCUMENTATION_PAGE);
+ }
+
+ /**
+ * Verifies that in an environment where no proxy variables are set, {@link NetworkProxy#configure()} does not configure proxy properties.
+ */
+ @Test
+ public void testNoProxy() {
+
+ // arrange
+ IdeTestContext context = new IdeTestContext();
+ NetworkProxy networkProxy = new NetworkProxy(context);
+
+ // act
+ networkProxy.configure();
+
+ // assert
+ assertThat(context.getSystem().getProperties()).isEmpty();
+ }
+
+ /**
+ * Verifies that in an environment where a HTTP_PROXY variable is set, {@link NetworkProxy#configure()} configures the according Java proxy properties.
+ */
+ @Test
+ public void testHttpProxyUpperCase() {
+
+ // arrange
+ IdeTestContext context = new IdeTestContext();
+ NetworkProxy networkProxy = new NetworkProxy(context);
+ context.getSystem().setEnv("HTTP_PROXY", "http://proxy.host.com:8888");
+ String noProxy = ".foo.com,localhost";
+ context.getSystem().setEnv("NO_PROXY", noProxy);
+
+ // act
+ networkProxy.configure();
+
+ // assert
+ assertThat(context.getSystem().getProperty("http.proxyHost")).isEqualTo("proxy.host.com");
+ assertThat(context.getSystem().getProperty("http.proxyPort")).isEqualTo("8888");
+ assertThat(context.getSystem().getProperty("http.nonProxyHosts")).isEqualTo(noProxy);
+ assertThat(context.getSystem().getProperties()).hasSize(3);
+ }
+
+ /**
+ * Verifies that in an environment where a http_proxy variable is set, {@link NetworkProxy#configure()} configures the according Java proxy properties.
+ */
+ @Test
+ public void testHttpProxyLowercase() {
+
+ // arrange
+ IdeTestContext context = new IdeTestContext();
+ NetworkProxy networkProxy = new NetworkProxy(context);
+ context.getSystem().setEnv("http_proxy", "http://proxy.host.com:8888");
+ String noProxy = ".foo.com,localhost";
+ context.getSystem().setEnv("no_proxy", noProxy);
+
+ // act
+ networkProxy.configure();
+
+ // assert
+ assertThat(context.getSystem().getProperty("http.proxyHost")).isEqualTo("proxy.host.com");
+ assertThat(context.getSystem().getProperty("http.proxyPort")).isEqualTo("8888");
+ assertThat(context.getSystem().getProperty("http.nonProxyHosts")).isEqualTo(noProxy);
+ assertThat(context.getSystem().getProperties()).hasSize(3);
+ }
+
+ /**
+ * Verifies that in an environment where a HTTPS_PROXY variable is set, {@link NetworkProxy#configure()} configures the according Java proxy properties.
+ * object.
+ */
+ @Test
+ public void testHttpsProxyUpperCase() {
+
+ // arrange
+ IdeTestContext context = new IdeTestContext();
+ NetworkProxy networkProxy = new NetworkProxy(context);
+ context.getSystem().setEnv("HTTPS_PROXY", "https://secure.proxy.com:8443");
+ String noProxy = ".foo.com,localhost";
+ context.getSystem().setEnv("NO_PROXY", noProxy);
+
+ // act
+ networkProxy.configure();
+
+ // assert
+ assertThat(context.getSystem().getProperty("https.proxyHost")).isEqualTo("secure.proxy.com");
+ assertThat(context.getSystem().getProperty("https.proxyPort")).isEqualTo("8443");
+ assertThat(context.getSystem().getProperty("https.nonProxyHosts")).isEqualTo(noProxy);
+ assertThat(context.getSystem().getProperties()).hasSize(3);
+ }
+
+ /**
+ * Verifies that in an environment where an all_proxy variable is set, {@link NetworkProxy#configure()} configures the according Java proxy properties.
+ * object.
+ */
+ @Test
+ public void testAllProxyLowerCase() {
+
+ // arrange
+ IdeTestContext context = new IdeTestContext();
+ NetworkProxy networkProxy = new NetworkProxy(context);
+ context.getSystem().setEnv("all_proxy", "https://secure.proxy.com");
+ String noProxy = ".foo.com,localhost";
+ context.getSystem().setEnv("no_proxy", noProxy);
+
+ // act
+ networkProxy.configure();
+
+ // assert
+ assertThat(context.getSystem().getProperty("http.proxyHost")).isEqualTo("secure.proxy.com");
+ assertThat(context.getSystem().getProperty("http.proxyPort")).isEqualTo("443");
+ assertThat(context.getSystem().getProperty("http.nonProxyHosts")).isEqualTo(noProxy);
+ assertThat(context.getSystem().getProperty("https.proxyHost")).isEqualTo("secure.proxy.com");
+ assertThat(context.getSystem().getProperty("https.proxyPort")).isEqualTo("443");
+ assertThat(context.getSystem().getProperty("https.nonProxyHosts")).isEqualTo(noProxy);
+ assertThat(context.getSystem().getProperties()).hasSize(6);
+ }
+
+ /**
+ * Verifies that in an environment where an ALL_PROXY variable is set, {@link NetworkProxy#configure()} configures the according Java proxy properties.
+ * object.
+ */
+ @Test
+ public void testAllProxyUpperCase() {
+
+ // arrange
+ IdeTestContext context = new IdeTestContext();
+ NetworkProxy networkProxy = new NetworkProxy(context);
+ context.getSystem().setEnv("ALL_PROXY", "http://proxy.company.com");
+ String noProxy = ".foo.com,localhost";
+ context.getSystem().setEnv("no_proxy", noProxy);
+
+ // act
+ networkProxy.configure();
+
+ // assert
+ assertThat(context.getSystem().getProperty("http.proxyHost")).isEqualTo("proxy.company.com");
+ assertThat(context.getSystem().getProperty("http.proxyPort")).isEqualTo("80");
+ assertThat(context.getSystem().getProperty("http.nonProxyHosts")).isEqualTo(noProxy);
+ assertThat(context.getSystem().getProperty("https.proxyHost")).isEqualTo("proxy.company.com");
+ assertThat(context.getSystem().getProperty("https.proxyPort")).isEqualTo("80");
+ assertThat(context.getSystem().getProperty("https.nonProxyHosts")).isEqualTo(noProxy);
+ assertThat(context.getSystem().getProperties()).hasSize(6);
+ }
+
+}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/network/ProxyContextTest.java b/cli/src/test/java/com/devonfw/tools/ide/network/ProxyContextTest.java
deleted file mode 100644
index 9697a8957..000000000
--- a/cli/src/test/java/com/devonfw/tools/ide/network/ProxyContextTest.java
+++ /dev/null
@@ -1,204 +0,0 @@
-package com.devonfw.tools.ide.network;
-
-import java.net.Proxy;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-
-import com.devonfw.tools.ide.context.AbstractIdeContextTest;
-import com.devonfw.tools.ide.context.IdeTestContext;
-
-import uk.org.webcompere.systemstubs.environment.EnvironmentVariables;
-import uk.org.webcompere.systemstubs.jupiter.SystemStub;
-import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension;
-
-/**
- * Test of {@link ProxyContext} and {@link ProxyConfig}.
- */
-@ExtendWith(SystemStubsExtension.class)
-public class ProxyContextTest extends AbstractIdeContextTest {
-
- private static final String PROJECT_PATH = "project/workspaces/foo-test/my-git-repo";
-
- private static final String HTTP_PROXY = "http://127.0.0.1:8888";
-
- private static final String HTTPS_PROXY = "https://127.0.0.1:8888";
-
- private static final String HTTP_PROXY_NO_HOST = "http://:8888";
- private static final String HTTP_PROXY_WRONG_HOST = "http://127.0.0.1wrongwrong:8888";
-
- private static final String HTTP_PROXY_WRONG_PROTOCOL = "wrong://127.0.0.1:8888";
-
- private static final String HTTP_PROXY_WRONG_FORMAT = "http://127.0.0.1:8888:wrong:wrong";
-
- private static final String PROXY_DOCUMENTATION_PAGE = "https://github.com/devonfw/IDEasy/blob/main/documentation/proxy-support.adoc";
-
- static final String PROXY_FORMAT_WARNING_MESSAGE =
- "Proxy configuration detected, but the formatting appears to be incorrect. Proxy configuration will be skipped.\n"
- + "Please note that IDEasy can detect a proxy only if the corresponding environmental variables are properly formatted. "
- + "For further details, see " + PROXY_DOCUMENTATION_PAGE;
-
- /**
- * Verifies that when the download URL is malformed, {@link ProxyContext#getProxy(String)} returns {@link Proxy#NO_PROXY}.
- */
- @Test
- public void testNoProxyMalformedUrl() {
-
- // act
- IdeTestContext context = newContext(PROJECT_BASIC, PROJECT_PATH, false);
- Proxy proxy = context.getProxyContext().getProxy("htt:p//example.com");
-
- // assert
- assertThat(proxy).isEqualTo(Proxy.NO_PROXY);
- }
-
- /**
- * Verifies that in an environment where no proxy variables are set, {@link ProxyContext#getProxy(String)} returns {@link Proxy#NO_PROXY}.
- */
- @Test
- public void testNoProxy() {
-
- // act
- IdeTestContext context = newContext(PROJECT_BASIC, PROJECT_PATH, false);
- Proxy proxy = context.getProxyContext().getProxy("https://example.com");
-
- // assert
- assertThat(proxy).isEqualTo(Proxy.NO_PROXY);
- }
-
- @SystemStub
- private final EnvironmentVariables environment = new EnvironmentVariables();
-
- /**
- * Verifies that in an environment where a http proxy variable is set, {@link ProxyContext#getProxy(String)} returns a correctly configured {@link Proxy}
- * object.
- */
- @Test
- public void testWithMockedHttpVar() {
-
- // arrange
- this.environment.set("HTTP_PROXY", HTTP_PROXY);
-
- // act
- IdeTestContext context = newContext(PROJECT_BASIC, PROJECT_PATH, false);
- Proxy proxy = context.getProxyContext().getProxy("http://example.com");
-
- // assert
- assertThat("http:/" + proxy.address().toString()).isEqualTo(HTTP_PROXY);
- assertThat(proxy.type()).isEqualTo(Proxy.Type.HTTP);
- }
-
- /**
- * Verifies that in an environment where a http proxy variable (lowercase) is set, {@link ProxyContext#getProxy(String)} returns a correctly configured
- * {@link Proxy} object.
- */
- @Test
- public void testWithMockedHttpVarLowercase() {
-
- // arrange
- this.environment.set("http_proxy", HTTP_PROXY);
-
- // act
- IdeTestContext context = newContext(PROJECT_BASIC, PROJECT_PATH, false);
- Proxy proxy = context.getProxyContext().getProxy("http://example.com");
-
- // assert
- assertThat("http:/" + proxy.address().toString()).isEqualTo(HTTP_PROXY);
- assertThat(proxy.type()).isEqualTo(Proxy.Type.HTTP);
- }
-
- /**
- * Verifies that in an environment where a https proxy variable is set, {@link ProxyContext#getProxy(String)} returns a correctly configured {@link Proxy}
- * object.
- */
- @Test
- public void testWithMockedHttpsVar() {
-
- // arrange
- this.environment.set("HTTPS_PROXY", HTTPS_PROXY);
-
- // act
- IdeTestContext context = newContext(PROJECT_BASIC, PROJECT_PATH, false);
- Proxy proxy = context.getProxyContext().getProxy("https://example.com");
-
- // assert
- assertThat("https:/" + proxy.address().toString()).isEqualTo(HTTPS_PROXY);
- assertThat(proxy.type()).isEqualTo(Proxy.Type.HTTP);
- }
-
- /**
- * Verifies that in an environment where a http proxy variable is wrongly formatted, {@link ProxyContext#getProxy(String)} returns {@link Proxy#NO_PROXY}. A
- * warning message is displayed.
- */
- @Test
- public void testWithMockedHttpVarWrongFormat() {
-
- // arrange
- this.environment.set("HTTP_PROXY", HTTP_PROXY_WRONG_FORMAT);
-
- // act
- IdeTestContext context = newContext(PROJECT_BASIC, PROJECT_PATH, false);
- Proxy proxy = context.getProxyContext().getProxy("http://example.com");
-
- // assert
- assertThat(proxy).isEqualTo(Proxy.NO_PROXY);
- assertThat(context).logAtWarning().hasMessage(PROXY_FORMAT_WARNING_MESSAGE);
- }
-
- /**
- * Verifies that in an environment where a http proxy variable is wrongly formatted, i.e. the host is empty, {@link ProxyContext#getProxy(String)} returns
- * {@link Proxy#NO_PROXY}.
- */
- @Test
- public void testWithMockedHttpVarNoHost() {
-
- // arrange
- this.environment.set("HTTP_PROXY", HTTP_PROXY_NO_HOST);
-
- // act
- IdeTestContext context = newContext(PROJECT_BASIC, PROJECT_PATH, false);
- Proxy proxy = context.getProxyContext().getProxy("http://example.com");
-
- // assert
- assertThat(proxy).isEqualTo(Proxy.NO_PROXY);
- }
-
- /**
- * Verifies that in an environment where a http proxy variable is wrongly formatted, {@link ProxyContext#getProxy(String)} returns {@link Proxy#NO_PROXY}. A
- * warning message is displayed.
- */
- @Test
- public void testWithMockedHttpVarWrongHost() {
-
- // arrange
- this.environment.set("HTTP_PROXY", HTTP_PROXY_WRONG_HOST);
-
- // act
- IdeTestContext context = newContext(PROJECT_BASIC, PROJECT_PATH, false);
- Proxy proxy = context.getProxyContext().getProxy("http://example.com");
-
- // assert
- assertThat(proxy).isEqualTo(Proxy.NO_PROXY);
- assertThat(context).logAtWarning().hasMessage(PROXY_FORMAT_WARNING_MESSAGE);
- }
-
- /**
- * Verifies that in an environment where a http proxy variable is wrongly formatted, {@link ProxyContext#getProxy(String)} returns {@link Proxy#NO_PROXY}. A
- * warning message is displayed.
- */
- @Test
- public void testWithMockedHttpVarWrongProtocol() {
-
- // arrange
- this.environment.set("HTTP_PROXY", HTTP_PROXY_WRONG_PROTOCOL);
-
- // act
- IdeTestContext context = newContext(PROJECT_BASIC, PROJECT_PATH, false);
- Proxy proxy = context.getProxyContext().getProxy("http://example.com");
-
- // assert
- assertThat(proxy).isEqualTo(Proxy.NO_PROXY);
- assertThat(context).logAtWarning().hasMessage(PROXY_FORMAT_WARNING_MESSAGE);
- }
-
-}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/property/CommandletPropertyTest.java b/cli/src/test/java/com/devonfw/tools/ide/property/CommandletPropertyTest.java
index 2c4318f86..969faf909 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/property/CommandletPropertyTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/property/CommandletPropertyTest.java
@@ -18,22 +18,31 @@ class CommandletPropertyTest {
@Test
public void testCompleteValue() {
+
+ // arrange
IdeContext context = IdeTestContextMock.get();
String[] expectedCandidates = { "help", "helm" };
String input = "he";
CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(context);
-
CommandletProperty cmdProp = new CommandletProperty("", false, "");
+
+ // act
cmdProp.completeValue(input, context, new ContextCommandlet(), collector);
+ // assert
assertThat(collector.getCandidates().stream().map(CompletionCandidate::text)).containsExactlyInAnyOrder(expectedCandidates);
}
@Test
public void testParse() {
+
+ // arrange
IdeContext context = IdeTestContextMock.get();
+
+ // act
CommandletProperty cmdProp = new CommandletProperty("", false, "");
+ // act + assert
assertThat(cmdProp.parse("help", context)).isInstanceOf(HelpCommandlet.class);
assertThat(cmdProp.parse("helm", context)).isInstanceOf(Helm.class);
assertThat(cmdProp.parse("intellij", context)).isInstanceOf(Intellij.class);
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 56b9bd0dd..23284999d 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
@@ -99,7 +99,7 @@ public void testInvalidUsageSuccessError() {
assertThat(context).log().hasEntries(IdeLogEntry.ofTrace("Starting step Test-Step..."),
IdeLogEntry.ofStep("Start: Test-Step"),
IdeLogEntry.ofWarning("Step 'Test-Step' already ended with true and now ended again with false."),
- IdeLogEntry.ofError("unexpected situation!"),
+ IdeLogEntry.ofError("java.lang.IllegalStateException: unexpected situation!"),
IdeLogEntry.ofDebug("Step 'Test-Step' ended with failure."));
}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/androidstudio/AndroidStudioTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/androidstudio/AndroidStudioTest.java
index e2ac4eabd..3e306bcee 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/tool/androidstudio/AndroidStudioTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/tool/androidstudio/AndroidStudioTest.java
@@ -82,7 +82,7 @@ private void checkInstallation(IdeTestContext context) {
assertThat(context.getSoftwarePath().resolve("android-studio/.ide.software.version")).exists().hasContent("2024.1.1.1");
assertThat(context).logAtSuccess().hasEntries("Successfully ended step 'Install plugin MockedPlugin'.", //
"Successfully installed android-studio in version 2024.1.1.1");
- assertThat(context.getPluginsPath().resolve("android-studio").resolve("mockedPlugin").resolve("MockedClass.class")).exists();
+ assertThat(context.getPluginsPath().resolve("android-studio").resolve("mockedPlugin").resolve("dev").resolve("MockedClass.class")).exists();
}
private void setupMockedPlugin(WireMockRuntimeInfo wmRuntimeInfo) throws IOException {
diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/ide/IdeToolDummyCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/ide/IdeToolDummyCommandletTest.java
new file mode 100644
index 000000000..1bf582688
--- /dev/null
+++ b/cli/src/test/java/com/devonfw/tools/ide/tool/ide/IdeToolDummyCommandletTest.java
@@ -0,0 +1,89 @@
+package com.devonfw.tools.ide.tool.ide;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import com.devonfw.tools.ide.commandlet.Commandlet;
+import com.devonfw.tools.ide.common.Tag;
+import com.devonfw.tools.ide.context.AbstractIdeContextTest;
+import com.devonfw.tools.ide.context.AbstractIdeTestContext;
+import com.devonfw.tools.ide.context.IdeContext;
+import com.devonfw.tools.ide.context.IdeSlf4jContext;
+import com.devonfw.tools.ide.process.ProcessErrorHandling;
+import com.devonfw.tools.ide.process.ProcessMode;
+import com.devonfw.tools.ide.process.ProcessResult;
+import com.devonfw.tools.ide.process.ProcessResultImpl;
+import com.devonfw.tools.ide.step.Step;
+import com.devonfw.tools.ide.tool.plugin.ToolPluginDescriptor;
+import com.devonfw.tools.ide.version.GenericVersionRange;
+
+/**
+ * Test of {@link IdeToolCommandlet} using {@link IdeToolDummyCommandlet}.
+ */
+public class IdeToolDummyCommandletTest extends AbstractIdeContextTest {
+
+ /**
+ * Run the dummy commandlet and test that only active plugins are passed to installPlugin method.
+ *
+ * @param tempDir the {@link TempDir}.
+ */
+ @Test
+ public void testDummyCommandlet(@TempDir Path tempDir) {
+
+ AbstractIdeTestContext context = new IdeSlf4jContext();
+ context.setPluginsPath(tempDir);
+ context.setSettingsPath(Path.of("src/test/resources/settings/dummy"));
+ IdeToolDummyCommandlet dummyCommandlet = new IdeToolDummyCommandlet(context, "dummy", Set.of(Tag.IDE));
+
+ context.addCommandlet(dummyCommandlet);
+
+ Commandlet dummy = context.getCommandletManager().getCommandlet("dummy");
+ assertThat(dummy).isSameAs(dummyCommandlet);
+ dummy.run();
+ assertThat(dummyCommandlet.installedPlugins).hasSize(1);
+ ToolPluginDescriptor plugin = dummyCommandlet.installedPlugins.get(0);
+ assertThat(plugin.id()).isEqualTo("plugin1-id");
+ assertThat(plugin.url()).isEqualTo("https://dummy.com/plugins/plugin1-url");
+ }
+
+ /**
+ * Dummy commandlet extending {@link IdeToolCommandlet} for testing.
+ */
+ public static class IdeToolDummyCommandlet extends IdeToolCommandlet {
+
+ final List installedPlugins;
+
+ IdeToolDummyCommandlet(IdeContext context, String tool, Set tags) {
+
+ super(context, tool, tags);
+ this.installedPlugins = new ArrayList<>();
+ }
+
+ @Override
+ protected void configureWorkspace() {
+
+ // disable workspace configuration since we have no IDE_HOME and therefore no settings
+ }
+
+ @Override
+ public ProcessResult runTool(ProcessMode processMode, GenericVersionRange toolVersion, ProcessErrorHandling errorHandling, String... args) {
+
+ // skip installation but trigger postInstall to test mocked plugin installation
+ postInstall(true);
+ return new ProcessResultImpl(this.tool, this.tool, 0, List.of(), List.of());
+ }
+
+ @Override
+ public void installPlugin(ToolPluginDescriptor plugin, Step step) {
+
+ this.installedPlugins.add(plugin);
+ step.success("Dummy plugin " + plugin.name() + " installed.");
+ }
+
+ }
+}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/intellij/IntellijTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/intellij/IntellijTest.java
index c57b4cd30..4e397634d 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/tool/intellij/IntellijTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/tool/intellij/IntellijTest.java
@@ -143,7 +143,7 @@ private void checkInstallation(IdeTestContext context) {
assertThat(context).logAtSuccess().hasEntries("Successfully installed java in version 17.0.10_7",
"Successfully installed intellij in version 2023.3.3");
assertThat(context).logAtSuccess().hasMessage("Successfully ended step 'Install plugin MockedPlugin'.");
- assertThat(context.getPluginsPath().resolve("intellij").resolve("mockedPlugin").resolve("MockedClass.class")).exists();
+ assertThat(context.getPluginsPath().resolve("intellij").resolve("mockedPlugin").resolve("dev").resolve("MockedClass.class")).exists();
}
private void setupMockedPlugin(WireMockRuntimeInfo wmRuntimeInfo, boolean mockedPluginActive) throws IOException {
diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/jasypt/JasyptTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/jasypt/JasyptTest.java
index cfe6980b3..cba9cb5a5 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/tool/jasypt/JasyptTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/tool/jasypt/JasyptTest.java
@@ -1,20 +1,14 @@
package com.devonfw.tools.ide.tool.jasypt;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
import com.devonfw.tools.ide.commandlet.InstallCommandlet;
import com.devonfw.tools.ide.context.AbstractIdeContextTest;
import com.devonfw.tools.ide.context.IdeTestContext;
-import uk.org.webcompere.systemstubs.environment.EnvironmentVariables;
-import uk.org.webcompere.systemstubs.jupiter.SystemStub;
-import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension;
-
/**
* Integration test of {@link Jasypt}.
*/
-@ExtendWith(SystemStubsExtension.class)
public class JasyptTest extends AbstractIdeContextTest {
private static final String JASYPT_OPTS = "custom_argument";
@@ -78,9 +72,6 @@ public void testJasyptRun() {
checkInstallation(context);
}
- @SystemStub
- private final EnvironmentVariables environment = new EnvironmentVariables();
-
/**
* Tests if {@link Jasypt} Commandlet is properly running with a user-defined JASYPT_OPTS env variable
*/
@@ -88,9 +79,8 @@ public void testJasyptRun() {
public void testJasyptRunWithCustomVariable() {
// arrange
- this.environment.set("JASYPT_OPTS", JASYPT_OPTS);
-
IdeTestContext context = newContext(PROJECT_JASYPT);
+ context.getSystem().setEnv("JASYPT_OPTS", JASYPT_OPTS);
Jasypt commandlet = new Jasypt(context);
commandlet.command.setValue(JasyptCommand.ENCRYPT);
diff --git a/cli/src/test/resources/settings/dummy/dummy/plugins/plugin1.properties b/cli/src/test/resources/settings/dummy/dummy/plugins/plugin1.properties
new file mode 100644
index 000000000..138d14b61
--- /dev/null
+++ b/cli/src/test/resources/settings/dummy/dummy/plugins/plugin1.properties
@@ -0,0 +1,4 @@
+plugin_id=plugin1-id
+plugin_active=true
+plugin_url=https://dummy.com/plugins/plugin1-url
+tags=dummy
diff --git a/cli/src/test/resources/settings/dummy/dummy/plugins/plugin2.properties b/cli/src/test/resources/settings/dummy/dummy/plugins/plugin2.properties
new file mode 100644
index 000000000..d7e1205af
--- /dev/null
+++ b/cli/src/test/resources/settings/dummy/dummy/plugins/plugin2.properties
@@ -0,0 +1,4 @@
+plugin_id=plugin2-id
+plugin_active=false
+plugin_url=https://dummy.com/plugins/plugin2-url
+tags=dummy2
diff --git a/documentation/coding-assistant.adoc b/documentation/coding-assistant.adoc
new file mode 100644
index 000000000..17c2ebc4b
--- /dev/null
+++ b/documentation/coding-assistant.adoc
@@ -0,0 +1,33 @@
+:toc:
+toc::[]
+
+= Coding Assistant
+The Coding Assistant is an AI-powered tool that helps you write and edit code more efficiently.
+It offers features like code suggestions, autocompletion, and error detection, improving productivity and reducing mistakes.
+It can be easily integrated into your development environment for smarter coding support.
+
+== Continue
+Continue is a leading open-source AI code assistant.
+It comes with features like https://docs.continue.dev/chat/how-to-use-it[Chat], https://docs.continue.dev/autocomplete/how-to-use-it[Autocomplete], https://docs.continue.dev/edit/how-to-use-it[Edit] and https://docs.continue.dev/actions/how-to-use-it[Actions].
+Currently, the plugin is available for Intellij and VSCode.
+With IDEasy, we aim to keep the configuration process simple and provide the option to pre-configure the code assistant via https://github.com/devonfw/ide-settings[ide-settings]
+
+A possible pre-configuration file can be found here: https://github.com/devonfw/ide-settings/workspace/update/.continuerc.json[.continuerc.json]
+
+The config is set so that your model runs via `vllm`.
+The only action you have to take is to override the `apiBase` key with your server url.
+Telemetry is also disabled by default.
+There is a variety of different configurations that can be done with `continue` plugin.
+You can find the latest information and documentation here: https://docs.continue.dev/
+```
+{
+ "models": [
+ {
+ "title": "My vLLM OpenAI-compatible server",
+ "apiBase": "http://localhost:8000/v1"
+ }
+ ],
+
+ "allowAnonymousTelemetry": false
+}
+```
diff --git a/documentation/github-release.adoc b/documentation/github-release.adoc
new file mode 100644
index 000000000..5f61a86cb
--- /dev/null
+++ b/documentation/github-release.adoc
@@ -0,0 +1,109 @@
+:toc:
+toc::[]
+
+= Building IDEasy Releases
+
+We have spent quite some effort to fully automate our release processes with github actions.
+This documentation aims to explain how it works.
+
+== Usage
+
+Usage is very simple:
+
+* Every night a SNAPSHOT release is build and deployed for internal developers and retesting of bugfixes.
+This process if fully automated.
+See link:setup.adoc#testing-snapshot-releases[testing-snapshot-releases] for details.
+* When all is prepared for the next release, we run the https://github.com/devonfw/IDEasy/actions/workflows/release.yml[release] workflow.
+With according permissions, you will see a gray `Run workflow` button that opens an overlay with a green `Run workflow` button that you need to press (correct branch `main` is preselected).
+That is all you need to create a new release.
+It will automatically appear on https://github.com/devonfw/IDEasy/releases[github releases].
+Please note that the staging process of the release to maven central has some delay so the download links may only work ~1h after the release job completed.
+
+After an official release has been published the following manual steps need to be performed:
+
+* Close the release milestone that has just been released on github (see https://github.com/devonfw/IDEasy/milestones[milestones] and pay attention to the links `Edit`, `Close`, and `Delete`).
+* Verify and potentially edit/update the new `SNAPSHOT` version in https://github.com/devonfw/IDEasy/blob/main/.mvn/maven.config[maven.config].
+The release workflow will always increase the last digit (micro-version) and often we instead want to increase the month segment.
+Example: the version was `2025.01.001-SNAPSHOT` and therefore the release `2025.01.001` has been build and deployed.
+Then the new version was set to `2025.01.002-SNAPSHOT`.
+In case that the January is almost over and the next release is planned for Februrary, you instead want to change the version to `2025.02.001-SNAPSHOT`.
+* Finally, you edit our https://github.com/devonfw/IDEasy/blob/main/CHANGELOG.adoc[CHANGELOG] and add the next planned version at the top.
+Include the header and footer lines but leave the content in between as a blank line (where the issues will be added as bullet-list).
+
+== How it works
+
+To make all this work a lot of different technical aspects work together.
+Here we consider fundamental things such as https://www.java.com[Java], https://maven.apache.org/[Maven], and https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions[Github Actions] as given.
+
+=== OSSRH
+
+* As a preparation we have requested https://central.sonatype.org/publish/publish-guide/[OSSRH] (Open Source Repository Hosting) for our project.
+* We have created according account and registered PGP key
+* We have added PGP key and account credentials to our github secrets (see also our https://github.com/devonfw/IDEasy/blob/main/.mvn/settings.xml[settings.xml] that is referenced from maven calls in github actions).
+* In our parent POM we have configured a https://github.com/devonfw/maven-parent/blob/9d31509d5f25c96fa1ec8b4f8cd2c341349b4df2/pom.xml#L290-L342[deploy profile] with additional steps for deploying via OSSRH
+
+=== Native Images
+
+* In order to make our CLI tool work fast and without having Java installed as a pre-requisite, we use https://www.graalvm.org/latest/reference-manual/native-image/[GraalVM native image] technology to compile our Java code to a platform specific binary.
+* To build such a binary, we have configured a https://github.com/devonfw/IDEasy/blob/10fc17b42ad4d465ee96fe5af7739d99a5132f51/cli/pom.xml#L204-L245[native profile].
+* This uses the https://graalvm.github.io/native-build-tools/latest/maven-plugin.html[native-maven-plugin] to actually build the native binary.
+* Since GraalVM does not support cross-platform compilation, we need to run such build on a (virtual) machine operated by the according OS.
+* The awesome github platform supports arbitrary operating systems and architectures to run builds on from github actions.
+* Therefore we run a job (`build-natives`) using a `strategy` with `matrix.os` configured to all the operating systems and architectures we support.
+The configured values are container image names that match to a specific platform but are a little cryptic.
+You can find a list of these container images https://github.com/actions/runner-images?tab=readme-ov-file#available-images[here].
+* This will spawn a build for each configured OS in a container on the according OS and architecture.
+All these builds will run in parallel.
+* We use the `setup-graalvm` action to make GraalVM available in the build container.
+* To build the actual native image, we simply invoke `compile` goal via maven but only in the `cli` module (`cd cli`).
+Here we need to activate the `native` profile (`-Pnative`).
+Also we can skip the tests since they are done in the main build anyway (`-DskipTests`).
+Since we want to build the native image for a specific version, we compute that version (e.g. by removing the `-SNAPSHOT` suffix in case of an official release) and override it via `revision` variable (`-Drevision=${...}`).
+* Only after all these spawned builds have completed successfully the main build continues with the next job (`release` or `deploy`) what we call the "Main Build".
+* In order to use the native image(s) that have been build on different machines, we use the https://github.com/actions/upload-artifact[upload-artifact] action to upload them to an github exchange store and https://github.com/actions/download-artifact[download-artifact] action to get them all into the main build.
+
+=== Main Build
+
+* Like with any maven project, we use the `deploy` goal via maven to compile, test, package, install and finally deploy our project.
+* Here, we build the entire project with all modules including the documentation that gets generated as PDF (see https://github.com/devonfw/docgen/[docgen] and https://github.com/devonfw/IDEasy/blob/main/documentation/pom.xml[documentation/pom.xml] for details).
+* Since the documentation PDF gets attached as artifact it is installed and deployed to the maven repository.
+* In the `cli` module the build we compile our code again with `javac` and run the automated tests.
+* However, the real compilation to the native image has already happened before (see above section).
+* Content that is rather static like script files can be found in https://github.com/devonfw/IDEasy/tree/main/cli/src/main/package[src/main/package].
+* We use a https://github.com/devonfw/IDEasy/blob/10fc17b42ad4d465ee96fe5af7739d99a5132f51/cli/pom.xml#L130-L170[specific resource configuration] to filter and copy such files to `target/package`.
+* Therefore, the main step here is the `Assembly` creating an release for every target platform (called by maven with `-Passembly,deploy`).
+* Finally, all artifacts are signed, installed and deployed to OSSRH nexus.
+* In case of an official release they are automatically staged to maven central and also published to our https://github.com/devonfw/IDEasy/releases[github releases] using the GitHub CLI (`gh release create`).
+* Also only for official releases, we also write changes to our version (see https://github.com/devonfw/IDEasy/blob/main/.mvn/maven.config[maven.config]) and commit them.
+* The bumped release version also is stored as annotated tag via git.
+* Also the next `SNAPSHOT` version is set and committed.
+* After all was successful we push our commits and the tag - in case the build failed, nothing will be pushed and commits will be lost.
+
+=== Assembly
+
+* In order to build a `.tar.gz` archive with all the content needed in a release, we have configured a https://github.com/devonfw/IDEasy/blob/10fc17b42ad4d465ee96fe5af7739d99a5132f51/cli/pom.xml#L177-L203[assembly profile].
+* This uses the https://maven.apache.org/plugins/maven-assembly-plugin/[maven-assembly-plugin] to build such compressed archive.
+* For each platform (OS and architecture), we have an according configuration file in https://github.com/devonfw/IDEasy/tree/main/cli/src/main/assembly[src/main/assembly].
+* The assembly descriptor file format is described https://maven.apache.org/plugins/maven-assembly-plugin/assembly.html[here].
+* Each such file includes the according native image.
+Therefore, the proper container image name from the `matrix.os` (see above) has to be referenced (see https://github.com/devonfw/IDEasy/blob/10fc17b42ad4d465ee96fe5af7739d99a5132f51/cli/src/main/assembly/release-linux-x64.xml#L17[here] for an example).
+* Additionally we reference the https://github.com/devonfw/IDEasy/blob/10fc17b42ad4d465ee96fe5af7739d99a5132f51/cli/src/main/assembly/release-linux-x64.xml#L24[package content] (see above) and configure exclusions to ensure that only content relevant for the according platform gets included (e.g. `*.bat` files are only included in Windows releases but not for Linux or Mac).
+* Also the configuration includes the documentation as PDF (see https://github.com/devonfw/IDEasy/blob/10fc17b42ad4d465ee96fe5af7739d99a5132f51/cli/src/main/assembly/release-linux-x64.xml#L10-L13[here]).
+
+=== Workflows
+
+And finally we put it all togehter as github action workflow:
+
+* https://github.com/devonfw/IDEasy/blob/main/.github/workflows/release.yml[release.yml] is the workflow for an official release.
+* https://github.com/devonfw/IDEasy/blob/main/.github/workflows/nightly-build.yml[nightly-build.yml] is the workflow for a SNAPSHOT release.
+* For the nightly-build we use a trick to skip the build if no changes happened to our git in the last 24h to avoid waste:
+We created another https://github.com/devonfw/IDEasy/blob/main/.github/workflows/check-for-updates.yml[check-for-updates.yml] workflow that runs every night and checks for such updates.
+Only if recent changes where pushed to git on `main`, the `nightly-build` job is triggered and otherwise the build ends without any further action.
+
+Both `release` and `nightly-build` workflow use the `workflow_dispatch` trigger allowing them to be run manually as described above.
+However, the `nightly-build` is typically only triggered from `check-for-updates` workflow automatically.
+But for testing some change with GraalVM specific behaviour during the day, we sometimes also trigger the `nightly-build` workflow manually.
+
+
+
+
diff --git a/documentation/proxy-support.adoc b/documentation/proxy-support.adoc
index 920ad8c71..bd81fb3a6 100644
--- a/documentation/proxy-support.adoc
+++ b/documentation/proxy-support.adoc
@@ -1,23 +1,67 @@
-[[proxy-support.adoc]]
-= Proxy support
-
:toc:
toc::[]
-IDEasy provides built-in support for automatic HTTP and HTTPS proxy recognition.
+= Proxy support
+
+In order to be usable and acceptable world-wide and in enterprise contexts, it is required that IDEasy provides support for network proxies.
+In case you are working in a company and can only access the Internet via an HTTP proxy, we support your use-case and this page gives details how to make it work.
-[[proxy-support.adoc_Configuring-Proxy-settings]]
== Configuring Proxy Settings
To enable automatic proxy recognition, users need to set the appropriate environment variables in their system, or check if they are already set.
These variables should be formatted as follows, lowercase or uppercase:
-[source,bash]
-----
-http_proxy=http://:
-# e.g. http_proxy=http://127.0.0.1:8888
-https_proxy=https://:
-# e.g. https_proxy=https://127.0.0.1:8888
-----
+```
+# example values for a proxy configuration
+http_proxy=http://proxy.host.com:8888
+https_proxy=https://proxy.host.com:8443
+no_proxy=.domain.com,localhost
+```
+
+Many famous tools like `wget`, `curl`, etc. honor these variables and work behind a proxy this way.
+This also applies for IDEasy so in a standard case, it will work for you out of the box.
+However, in case it is not working, please read on to find solutions to configure IDEasy to your needs.
+
+== Advanced Proxy Configuration
+
+To support advanced proxy configuration, we introduced the link:variables.adoc[variable] `IDE_OPTIONS` that you can set on OS level or e.g. in your `~/.bashrc`.
+It allows to set arbitrary JVM options like https://docs.oracle.com/en/java/javase/21/core/java-networking.html#JSCOR-GUID-2C88D6BD-F278-4BD5-B0E5-F39B2BFAA840[proxy settings]
+as well as https://www.baeldung.com/java-custom-truststore[truststore settings] (see also https://docs.oracle.com/en/java/javase/21/docs/api/system-properties.html[Java system properties]).
+
+E.g. if you do not want to rely on the proxy environment variables above, you can also make this explicitly:
+
+```
+export IDE_OPTIONS="-Dhttps.proxyHost=proxy.host.com -Dhttps.proxyPort=8443"
+```
+
+=== Authentication
+
+In some cases your network proxy may require authentication.
+Then you need to manually configure your account details like in the following example:
+
+```
+export IDE_OPTIONS="-Dhttps.proxyUser=$USERNAME -Dhttps.proxyPassword=«password»"
+```
+
+=== Truststore
+
+Some strange VPN tools have the bad habit to break up and sniff TLS encrypted connections.
+Therefore, they create their own TLS connection with a self-made certificate that is typically installed into the certificate trust store of the OS during installation.
+However, tools like Java or Firefox do not use the OS trust store but bring their own and therefore may reveal this hack.
+In IDEasy (or Eclipse Marketplace) you may therefore end up with the following error:
+
+```
+javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
+```
+
+So in other words, you may want to create a proper https://www.baeldung.com/java-keystore-truststore-difference#java-truststore[truststore] and configure IDEasy like this:
+
+```
+export IDE_OPTIONS="-Djavax.net.ssl.trustStore=/path/to/another/truststore.p12 -Djavax.net.ssl.trustStorePassword=changeit"
+```
-IDEasy utilizes these environment variables to detect and configure proxy settings during runtime.
+Sorry, that we cannot support you automatically on this use-case.
+Ask your VPN tool vendor for support and why this is all required.
+In general encryption should be end-to-end and your data should be protected.
+You may also want to visit https://badssl.com/ while your VPN tool is active and click the certificate tests like https://pinning-test.badssl.com/[pinning-test].
+If you then do not get an error in your browser (like "Secure connection failed") but a red warning page, your VPN tools is putting you at risk with breaking your TLS connections.
diff --git a/documentation/repository.adoc b/documentation/repository.adoc
index 9823c3453..49891cb44 100644
--- a/documentation/repository.adoc
+++ b/documentation/repository.adoc
@@ -3,7 +3,7 @@ toc::[]
= Repository
-`IDEasy` supports to automatically check out and import required git repositories into your IDE during link:project.adoc[project] creation.
+`IDEasy` supports to automatically check out and import required git repositories (with your source-code) into your IDE during link:project.adoc[project] creation.
NOTE: Please do not mix this feature with the link:settings.adoc[settings repository] that contains the configuration for your IDE.
Here we are talking about git repositories for development in your project that typically contain code.
@@ -11,8 +11,7 @@ Here we are talking about git repositories for development in your project that
To configure this you put a `.properties` file for each desired project into the `repository` sub-folder in your link:settings.adoc[settings].
Each `.properties` file describes one "project" which you would like to check out and (potentially) import:
-[source,properties]
-----
+```
path=myproject
workingsets=Set1,Set2
workspace=example
@@ -22,11 +21,10 @@ build_path=.
build_cmd=mvn -DskipTests=true -Darchetype.test.skip=true clean install
import=eclipse
active=true
-----
+```
- .Variables of repository import
+.Variables of repository import
[options="header"]
-
|===
|*Variable*|*Value*|*Meaning*
|`path`|e.g. `myproject`, will clone into `${WORKSPACE_PATH}/myproject`|(required) Path into which the projects is cloned.
diff --git a/documentation/settings.adoc b/documentation/settings.adoc
index 4b86a9974..721404568 100644
--- a/documentation/settings.adoc
+++ b/documentation/settings.adoc
@@ -43,6 +43,9 @@ The settings folder (see `link:variables.adoc[SETTINGS_PATH]`) has to follow thi
│ └──/ https://github.com/devonfw/ide-settings/tree/main/vscode/workspace[workspace]
│ ├──/ https://github.com/devonfw/ide-settings/tree/main/vscode/workspace/setup[setup]
│ └──/ https://github.com/devonfw/ide-settings/tree/main/vscode/workspace/update[update]
+├──/ https://github.com/devonfw/ide-settings/tree/main/repositories[repositories]
+│ ├──/ ...
+│ └──/ https://github.com/devonfw/ide-settings/blob/main/repositories/README.adoc[README.adoc]
├──/ ...
└── https://github.com/devonfw/ide-settings/blob/main/ide.properties[ide.properties]
----
diff --git a/documentation/tool-vendor-plea.adoc b/documentation/tool-vendor-plea.adoc
index f9cc6207a..5136ba5dd 100644
--- a/documentation/tool-vendor-plea.adoc
+++ b/documentation/tool-vendor-plea.adoc
@@ -60,6 +60,25 @@ That is great!
But please follow best-practices and common sense and publish them as a new release with a new unique and increased version.
* Tools that have been published and then after some time the download disappeared leading to errors like 404
+=== Proper file format
+
+On Linux and Mac binaries and scripts need executable permission to be run (`chmod a+x «binary»`).
+Theoretically ZIP files can also store such metadata but this was added to ZIP format very late.
+Also ZIP is commonly used on Windows while other platforms prefer to use TAR-based archives.
+Therefore, many ZIP libraries are not supporting this feature at all or if they do they do it very poor.
+Many users and developers do not even know that ZIP files can actually store file permissions with executable flags.
+We started with apache commons-compress that unfortunetaly is already https://issues.apache.org/jira/browse/COMPRESS-562[buggy on simple extract].
+Then we discovered that `java.util.zip` from JDK https://stackoverflow.com/questions/54654170/what-are-the-extra-bytes-in-the-zipentry-used-for[cannot give reliable access to metadata such as executable flags].
+Finally, we found https://bugs.openjdk.org/browse/JDK-8213082[JDK-8213082] that lead us to the solution but caused many pitfalls and headaches until we came up with a unpacking of ZIP files that reliably preserves file permissions.
+
+As a best practice never use ZIP but instead something like `*.tar.gz` since TAR was designed with POSIX permissions from the start and then things just work.
+
+Unfortunately many famous tools violate this principle and provide ZIP files for Linux and Mac.
+As a result users unzip the package and then get a warm welcome when running the tool:
+```
+bash: «binary»: Permission denied
+```
+
=== Comparable versions
We really do not want to dicatate anything regarding your versioning scheme.
@@ -144,7 +163,7 @@ Also a good convention is the end options argument (`--`) that e.g. allows you t
Every tool should have the CLI option to print its version via `-v` or `--version`.
Please note that Java used to have `-version` instead of `--version` but later added support also for the latter (thanks guys!).
-Further, invoking this feature should *printing the version number and nothing else*.
+Further, invoking this feature should *print the version number and nothing else*.
Please consider that often you need to make a choice on a version in a shell script and tool vendors make life for this use-case unneccesary hard if they print lots of other information alongside.
If you want to do this add an extra option (e.g. `--verbose --version`) for this but *never* print it when `-v` was given as only argument.
diff --git a/documentation/variables.adoc b/documentation/variables.adoc
index ef203146c..cf3b9d3a3 100644
--- a/documentation/variables.adoc
+++ b/documentation/variables.adoc
@@ -15,6 +15,7 @@ Please note that we are trying to minimize any potential side-effect from `IDEas
|*Variable*|*Value*|*Meaning*
|`IDE_ROOT`|e.g. `/projects/` or `C:\projects`|The installation root directory of `IDEasy` - see link:structure.adoc[structure] for details.
|`IDE_HOME`|e.g. `/projects/my-project`|The top level directory of your `IDEasy` project.
+|`IDE_OPTIONS`|`-`|General options that will be applied to each call of `IDEasy`. Should typically be used for JVM options like link:proxy-support.adoc[proxy-support].
|`PATH`|`$IDE_HOME/software/java:...:$PATH`|You system path is adjusted by `ide` link:cli.adoc[command].
|`HOME_DIR`|`~`|The platform independent home directory of the current user. In some edge-cases (e.g. in cygwin) this differs from `~` to ensure a central home directory for the user on a single machine in any context or environment.
|`IDE_TOOLS`|`(java mvn node npm)`|List of tools that should be installed by default on project creation.
diff --git a/gui/pom.xml b/gui/pom.xml
index b1265c305..25808d681 100644
--- a/gui/pom.xml
+++ b/gui/pom.xml
@@ -10,9 +10,8 @@
../pom.xml
com.devonfw.tools.IDEasy
- gui
+ ide-gui
${revision}
- gui
com.devonfw.ide.gui.AppLauncher
diff --git a/pom.xml b/pom.xml
index beeccff3e..fc6b88b72 100644
--- a/pom.xml
+++ b/pom.xml
@@ -35,18 +35,6 @@
3.24.2
test
-
- org.mockito
- mockito-core
- 5.10.0
- test
-
-
- uk.org.webcompere
- system-stubs-jupiter
- 2.1.3
- test
-