From c9933974ae7d33639e3cdcd698a6a1ad56f0d06e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Hohwiller?= Date: Thu, 28 Nov 2024 19:24:16 +0100 Subject: [PATCH 01/15] #778: add icd #779: use functions instead of alias #810: setup in current shell #782: fix IDE_ROOT on linux/mac #774: fixed HTTP proxy support (#811) --- CHANGELOG.adoc | 9 + cli/pom.xml | 24 ++- .../tools/ide/context/AbstractIdeContext.java | 43 ++-- .../devonfw/tools/ide/context/IdeContext.java | 7 +- .../EnvironmentVariablesSystem.java | 2 +- .../tools/ide/environment/IdeSystem.java | 44 ++++ .../tools/ide/environment/IdeSystemImpl.java | 80 +++++++ .../devonfw/tools/ide/io/FileAccessImpl.java | 10 - .../tools/ide/network/NetworkProxy.java | 126 +++++++++++ .../tools/ide/network/ProxyConfig.java | 47 ---- .../tools/ide/network/ProxyContext.java | 96 --------- .../tools/ide/process/ProcessContextImpl.java | 35 +-- cli/src/main/package/bin/ide | 29 --- cli/src/main/package/completion | 11 - cli/src/main/package/functions | 94 ++++++++ cli/src/main/package/setup | 33 ++- .../ide/context/AbstractIdeTestContext.java | 19 ++ .../ide/environment/IdeSystemTestImpl.java | 59 +++++ .../tools/ide/network/NetworkProxyTest.java | 178 +++++++++++++++ .../tools/ide/network/ProxyContextTest.java | 204 ------------------ .../tools/ide/tool/jasypt/JasyptTest.java | 12 +- documentation/proxy-support.adoc | 70 ++++-- documentation/variables.adoc | 1 + pom.xml | 12 -- 24 files changed, 754 insertions(+), 491 deletions(-) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/environment/IdeSystem.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/environment/IdeSystemImpl.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/network/NetworkProxy.java delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/network/ProxyConfig.java delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/network/ProxyContext.java delete mode 100644 cli/src/main/package/bin/ide delete mode 100755 cli/src/main/package/completion create mode 100644 cli/src/main/package/functions create mode 100644 cli/src/test/java/com/devonfw/tools/ide/environment/IdeSystemTestImpl.java create mode 100644 cli/src/test/java/com/devonfw/tools/ide/network/NetworkProxyTest.java delete mode 100644 cli/src/test/java/com/devonfw/tools/ide/network/ProxyContextTest.java diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 5a987494f..d0185dcf9 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -4,9 +4,18 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE == 2024.12.001 +NOTE: ATTENTION: When installing this release as an update, you need to manually remove IDEasy entries from `.bashrc` and if present also `.zshrc`. +Also you should delete all files from your `$IDE_ROOT/_ide` folder before extracting the new version to it. +Then run the `setup` and all should work fine. + Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/774[#774]: HTTP proxy support not working properly * https://github.com/devonfw/IDEasy/issues/589[#589]: Fix NLS Bundles for Linux and MacOS +* https://github.com/devonfw/IDEasy/issues/778[#778]: Add icd command +* https://github.com/devonfw/IDEasy/issues/779[#779]: Consider functions instead of alias +* https://github.com/devonfw/IDEasy/issues/810[#810]: setup not adding IDEasy to current shell +* https://github.com/devonfw/IDEasy/issues/782[#782]: Fix IDE_ROOT variable on Linux * https://github.com/devonfw/IDEasy/issues/637[#637]: Added option to disable updates * https://github.com/devonfw/IDEasy/issues/764[#764]: IDEasy not working properly in CMD * https://github.com/devonfw/IDEasy/issues/81[#81]: Implement Toolcommandlet for Kubernetes diff --git a/cli/pom.xml b/cli/pom.xml index 34b3c44cf..5fc508d9a 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -68,13 +68,6 @@ 1.5.3 - - - com.github.tomakehurst - wiremock-jre8 - 2.35.1 - test - me.tongfei progressbar @@ -90,6 +83,21 @@ jansi ${jansi.version} + + + + org.mockito + mockito-core + 5.10.0 + test + + + + com.github.tomakehurst + wiremock-jre8 + 2.35.1 + test + net.bytebuddy @@ -102,11 +110,13 @@ org.xmlunit xmlunit-core 2.10.0 + test org.xmlunit xmlunit-assertj3 2.9.1 + test diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java index 06bf22e66..657147de1 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java @@ -28,13 +28,15 @@ import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables; 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.environment.IdeSystemImpl; import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.io.FileAccessImpl; import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.log.IdeLogger; import com.devonfw.tools.ide.log.IdeSubLogger; import com.devonfw.tools.ide.merge.DirectoryMerger; -import com.devonfw.tools.ide.network.ProxyContext; +import com.devonfw.tools.ide.network.NetworkProxy; import com.devonfw.tools.ide.os.SystemInfo; import com.devonfw.tools.ide.os.SystemInfoImpl; import com.devonfw.tools.ide.os.WindowsPathSyntax; @@ -124,6 +126,10 @@ public abstract class AbstractIdeContext implements IdeContext { protected Boolean online; + protected IdeSystem system; + + private NetworkProxy networkProxy; + /** * The constructor. * @@ -202,8 +208,8 @@ private Path findIdeRoot(Path ideHomePath) { return ideRootPath; } - private static Path getIdeRootPathFromEnv() { - String root = System.getenv(IdeVariables.IDE_ROOT.getName()); + private Path getIdeRootPathFromEnv() { + String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName()); if (root != null) { Path rootPath = Path.of(root); if (Files.isDirectory(rootPath)) { @@ -242,7 +248,7 @@ public void setCwd(Path userDir, String workspace, Path ideHome) { this.userHome = this.ideHome.resolve("home"); } } else { - this.userHome = Path.of(System.getProperty("user.home")); + this.userHome = Path.of(getSystem().getProperty("user.home")); } this.userHomeIde = this.userHome.resolve(".ide"); this.downloadPath = this.userHome.resolve("Downloads/ide"); @@ -261,8 +267,8 @@ private String getMessageIdeHomeNotFound() { return "You are not inside an IDE installation: " + this.cwd; } - private static String getMessageIdeRootNotFound() { - String root = System.getenv("IDE_ROOT"); + private String getMessageIdeRootNotFound() { + String root = getSystem().getEnv("IDE_ROOT"); if (root == null) { return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable."; } else { @@ -349,6 +355,8 @@ public SystemInfo getSystemInfo() { @Override public FileAccess getFileAccess() { + // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class + configureNetworkProxy(); return this.fileAccess; } @@ -547,6 +555,7 @@ public boolean isSkipUpdatesMode() { public boolean isOnline() { if (this.online == null) { + configureNetworkProxy(); // we currently assume we have only a CLI process that runs shortly // therefore we run this check only once to save resources when this method is called many times try { @@ -564,6 +573,13 @@ public boolean isOnline() { return this.online.booleanValue(); } + private void configureNetworkProxy() { + if (this.networkProxy == null) { + this.networkProxy = new NetworkProxy(this); + this.networkProxy.configure(); + } + } + @Override public Locale getLocale() { @@ -602,12 +618,6 @@ public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) { } } - @Override - public ProxyContext getProxyContext() { - - return new ProxyContext(this); - } - @Override public GitContext getGitContext() { @@ -624,6 +634,15 @@ public ProcessContext newProcess() { return processContext; } + @Override + public IdeSystem getSystem() { + + if (this.system == null) { + this.system = new IdeSystemImpl(this); + } + return this.system; + } + /** * @return a new instance of {@link ProcessContext}. * @see #newProcess() 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..3ca74ea16 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,10 @@ 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.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; @@ -416,7 +416,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. 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/io/FileAccessImpl.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java index 9860df712..2ed74428e 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,9 +6,6 @@ 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; @@ -82,12 +79,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 +96,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); } 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 24cec551a..d0ab88f23 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 @@ -172,27 +172,30 @@ public ProcessResult run(ProcessMode processMode) { Process process = this.processBuilder.start(); - if (processMode == ProcessMode.DEFAULT_CAPTURE) { - CompletableFuture> outFut = readInputStream(process.getInputStream()); - CompletableFuture> errFut = readInputStream(process.getErrorStream()); - 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(); - } - - ProcessResult result = new ProcessResultImpl(exitCode, out, err); + if (processMode.isBackground()) { + exitCode = ProcessResult.SUCCESS; + } else { + exitCode = process.waitFor(); + } - performLogging(result, exitCode, interpreter); + ProcessResult result = new ProcessResultImpl(exitCode, out, err); - return result; + performLogging(result, exitCode, interpreter); + return result; + } finally { + process.destroy(); + } } catch (CliProcessException | IllegalStateException e) { // these exceptions are thrown from performLogOnError and we do not want to wrap them (see #593) throw e; diff --git a/cli/src/main/package/bin/ide b/cli/src/main/package/bin/ide deleted file mode 100644 index 58ee8a3d0..000000000 --- a/cli/src/main/package/bin/ide +++ /dev/null @@ -1,29 +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 - ide status - 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/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..c5fdf0975 --- /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[93mInvalid 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..b1e90a4b5 100755 --- a/cli/src/main/package/setup +++ b/cli/src/main/package/setup @@ -6,34 +6,27 @@ 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 +fi +echo "Setting up IDEasy in ${PWD}" +cd .. +export IDE_ROOT=${PWD} +source "$IDE_ROOT/_ide/functions" doSetupInConfigFile ~/.bashrc doSetupInConfigFile ~/.zshrc 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 540940f8f..2d283b863 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 @@ -13,6 +13,8 @@ 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; @@ -128,6 +130,23 @@ protected AbstractEnvironmentVariables createSystemVariables() { return super.createSystemVariables(); } + @Override + public IdeSystemTestImpl getSystem() { + + if (this.system == null) { + this.system = IdeSystemTestImpl.ofSystemDefaults(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() { 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/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/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/documentation/proxy-support.adoc b/documentation/proxy-support.adoc index 920ad8c71..9d0570b5f 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="-Dhttp.proxyUser=$USERNAME -Dhttp.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/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/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 - From b2df9f749f045d713514779c8a85a333ede6b181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Hohwiller?= Date: Thu, 28 Nov 2024 19:59:48 +0100 Subject: [PATCH 02/15] #810: added warning message to setup explaining that a new shell needs to be started to use IDEasy (#819) --- cli/src/main/package/functions | 2 +- cli/src/main/package/setup | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/src/main/package/functions b/cli/src/main/package/functions index c5fdf0975..6774de33f 100644 --- a/cli/src/main/package/functions +++ b/cli/src/main/package/functions @@ -30,7 +30,7 @@ function icd() { ide init return elif [ $# -gt 2 ]; then - echo -e "\033[93mInvalid usage icd $*\033[39m" >&2 + echo -e "\033[91mInvalid usage icd $*\033[39m" >&2 return 1 fi if [ "$1" = "-p" ]; then diff --git a/cli/src/main/package/setup b/cli/src/main/package/setup index b1e90a4b5..c37cef0ee 100755 --- a/cli/src/main/package/setup +++ b/cli/src/main/package/setup @@ -22,6 +22,7 @@ function doSetupInConfigFile() { cd "$(dirname "${BASH_SOURCE:-$0}")" || exit 255 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 .. @@ -30,3 +31,5 @@ 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 From 76124f73b5008293ca845d94c65850c648e2a0d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Hohwiller?= Date: Fri, 29 Nov 2024 11:11:19 +0100 Subject: [PATCH 03/15] #792: added to CHANGELOG --- CHANGELOG.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index d0185dcf9..a4289c0df 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -11,6 +11,7 @@ Then run the `setup` and all should work fine. Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/774[#774]: HTTP proxy support not working properly +* https://github.com/devonfw/IDEasy/issues/792[#792]: Honor new variable IDE_OPTIONS in ide command wrapper * https://github.com/devonfw/IDEasy/issues/589[#589]: Fix NLS Bundles for Linux and MacOS * https://github.com/devonfw/IDEasy/issues/778[#778]: Add icd command * https://github.com/devonfw/IDEasy/issues/779[#779]: Consider functions instead of alias From 3897b6740bcf862e68866f99ae87cfe410d9daaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Hohwiller?= Date: Fri, 29 Nov 2024 12:27:00 +0100 Subject: [PATCH 04/15] #792: added support for IDE_OPTIONS also in CMD via ide.bat (#823) --- cli/src/main/package/bin/ide.bat | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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= Set _RESET= 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 ( From ae9ec5bcb9b7f4f3cb9f7bfe8bde727d4d2d29c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Hohwiller?= Date: Fri, 29 Nov 2024 12:46:30 +0100 Subject: [PATCH 05/15] #569: improve Validation (added JavaDoc, etc.) (#817) --- .../tools/ide/context/AbstractIdeContext.java | 3 +- .../ide/validation/PropertyValidator.java | 9 ++++++ .../ide/validation/ValidationResult.java | 9 ++++++ .../ide/validation/ValidationResultValid.java | 29 +++++++++++++++++++ .../tools/ide/validation/ValidationState.java | 23 ++++++++++++++- 5 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/validation/ValidationResultValid.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java index 657147de1..223e03278 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java @@ -52,6 +52,7 @@ import com.devonfw.tools.ide.step.StepImpl; import com.devonfw.tools.ide.url.model.UrlMetadata; import com.devonfw.tools.ide.validation.ValidationResult; +import com.devonfw.tools.ide.validation.ValidationResultValid; import com.devonfw.tools.ide.validation.ValidationState; import com.devonfw.tools.ide.variable.IdeVariables; @@ -994,7 +995,7 @@ public ValidationResult apply(CliArguments arguments, Commandlet cmd, Completion } currentArgument = arguments.current(); } - return new ValidationState(null); + return ValidationResultValid.get(); } @Override 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) { From 4ff2947c9a76ce4cac26c306ca27aba6ce4808d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Hohwiller?= Date: Fri, 29 Nov 2024 13:34:41 +0100 Subject: [PATCH 06/15] #758: improve status commandlet (#816) --- .../ide/commandlet/StatusCommandlet.java | 53 +++++++++++++++++++ .../tools/ide/context/AbstractIdeContext.java | 23 ++++++-- .../devonfw/tools/ide/context/IdeContext.java | 5 ++ .../AbstractEnvironmentVariables.java | 6 +++ .../ide/environment/EnvironmentVariables.java | 5 ++ .../EnvironmentVariablesPropertiesFile.java | 16 ++++++ 6 files changed, 103 insertions(+), 5 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java index 0f2e5b59c..1513187fd 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java @@ -1,6 +1,10 @@ package com.devonfw.tools.ide.commandlet; +import java.nio.file.Path; + +import com.devonfw.tools.ide.context.GitContext; import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.environment.EnvironmentVariables; /** * {@link Commandlet} to print a status report about IDEasy. @@ -26,5 +30,54 @@ public String getName() { @Override public void run() { + + this.context.logIdeHomeAndRootStatus(); + logOnlineStatus(); + logSettingsGitStatus(); + logSettingsLegacyStatus(); + } + + private void logSettingsLegacyStatus() { + EnvironmentVariables variables = this.context.getVariables(); + boolean hasLegacyProperties = false; + while (variables != null) { + Path legacyProperties = variables.getLegacyPropertiesFilePath(); + if (legacyProperties != null) { + hasLegacyProperties = true; + this.context.warning("Found legacy properties {}", legacyProperties); + } + variables = variables.getParent(); + } + if (hasLegacyProperties) { + this.context.warning( + "Your settings are outdated and contain legacy configurations. Please consider upgrading your settings:\nhttps://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc#upgrade"); + } + } + + private void logSettingsGitStatus() { + Path settingsPath = this.context.getSettingsPath(); + if (settingsPath != null) { + GitContext gitContext = this.context.getGitContext(); + if (gitContext.isRepositoryUpdateAvailable(settingsPath)) { + this.context.warning("Your settings are not up-to-date, please run 'ide update'."); + } else { + this.context.success("Your settings are up-to-date."); + } + String branch = gitContext.determineCurrentBranch(settingsPath); + this.context.debug("Your settings branch is {}", branch); + if (!"master".equals(branch) && !"main".equals(branch)) { + this.context.warning("Your settings are on a custom branch: {}", branch); + } + } + } + + private void logOnlineStatus() { + if (this.context.isOfflineMode()) { + this.context.warning("You have configured offline mode via CLI."); + } else if (this.context.isOnline()) { + this.context.success("You are online."); + } else { + this.context.warning("You are offline. Check your internet connection and potential proxy settings."); + } } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java index 223e03278..28f8f0a7e 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java @@ -659,6 +659,19 @@ public IdeSubLogger level(IdeLogLevel level) { return this.startContext.level(level); } + @Override + public void logIdeHomeAndRootStatus() { + + if (this.ideRoot != null) { + success("IDE_ROOT is set to {}", this.ideRoot); + } + if (this.ideHome == null) { + warning(getMessageIdeHomeNotFound()); + } else { + success("IDE_HOME is set to {}", this.ideHome); + } + } + @Override public String askForInput(String message, String defaultValue) { @@ -875,11 +888,11 @@ private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) { if (cmd.isIdeHomeRequired()) { debug(getMessageIdeHomeFound()); } - } - if (this.settingsPath != null) { - if (getGitContext().isRepositoryUpdateAvailable(this.settingsPath) || - (getGitContext().fetchIfNeeded(this.settingsPath) && getGitContext().isRepositoryUpdateAvailable(this.settingsPath))) { - interaction("Updates are available for the settings repository. If you want to pull the latest changes, call ide update."); + if (this.settingsPath != null) { + if (getGitContext().isRepositoryUpdateAvailable(this.settingsPath) || + (getGitContext().fetchIfNeeded(this.settingsPath) && getGitContext().isRepositoryUpdateAvailable(this.settingsPath))) { + interaction("Updates are available for the settings repository. If you want to pull the latest changes, call ide update."); + } } } cmd.run(); 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 3ca74ea16..787a38d64 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 @@ -545,4 +545,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/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) { From cd88c4f2da9129162ff45cb932806dbc88bdc2c1 Mon Sep 17 00:00:00 2001 From: JanAmeis <162981643+WorkingAmeise@users.noreply.github.com> Date: Fri, 29 Nov 2024 13:48:41 +0100 Subject: [PATCH 07/15] #587: check for git installation (#769) --- CHANGELOG.adoc | 1 + .../tools/ide/context/GitContextImpl.java | 32 +++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index a4289c0df..8cb546e92 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -14,6 +14,7 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/792[#792]: Honor new variable IDE_OPTIONS in ide command wrapper * https://github.com/devonfw/IDEasy/issues/589[#589]: Fix NLS Bundles for Linux and MacOS * https://github.com/devonfw/IDEasy/issues/778[#778]: Add icd command +* https://github.com/devonfw/IDEasy/issues/587[#587]: Checks for git installation before performing git operations * https://github.com/devonfw/IDEasy/issues/779[#779]: Consider functions instead of alias * https://github.com/devonfw/IDEasy/issues/810[#810]: setup not adding IDEasy to current shell * https://github.com/devonfw/IDEasy/issues/782[#782]: Fix IDE_ROOT variable on Linux 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 index 23613c117..24afe7cfa 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/GitContextImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/GitContextImpl.java @@ -53,7 +53,7 @@ public boolean fetchIfNeeded(Path targetRepository, String remote, String branch @Override public boolean isRepositoryUpdateAvailable(Path repository) { - + verifyGitInstalled(); 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."); @@ -137,7 +137,7 @@ private void handleErrors(Path targetRepository, ProcessResult result) { @Override public void clone(GitUrl gitRepoUrl, Path targetRepository) { - + verifyGitInstalled(); GitUrlSyntax gitUrlSyntax = IdeVariables.PREFERRED_GIT_PROTOCOL.get(getContext()); gitRepoUrl = gitUrlSyntax.format(gitRepoUrl); this.processContext.directory(targetRepository); @@ -169,7 +169,7 @@ public void clone(GitUrl gitRepoUrl, Path targetRepository) { @Override public void pull(Path repository) { - + verifyGitInstalled(); if (this.context.isOffline()) { this.context.info("Skipping git pull on {} because offline", repository); return; @@ -184,7 +184,7 @@ public void pull(Path repository) { @Override public void fetch(Path targetRepository, String remote, String branch) { - + verifyGitInstalled(); if (branch == null) { branch = determineCurrentBranch(targetRepository); } @@ -200,7 +200,7 @@ public void fetch(Path targetRepository, String remote, String branch) { @Override public String determineCurrentBranch(Path repository) { - + verifyGitInstalled(); ProcessResult remoteResult = this.processContext.directory(repository).addArg("branch").addArg("--show-current").run(ProcessMode.DEFAULT_CAPTURE); if (remoteResult.isSuccessful()) { List remotes = remoteResult.getOut(); @@ -216,7 +216,7 @@ public String determineCurrentBranch(Path repository) { @Override public String determineRemote(Path repository) { - + verifyGitInstalled(); ProcessResult remoteResult = this.processContext.directory(repository).addArg("remote").run(ProcessMode.DEFAULT_CAPTURE); if (remoteResult.isSuccessful()) { List remotes = remoteResult.getOut(); @@ -232,7 +232,7 @@ public String determineRemote(Path repository) { @Override public void reset(Path targetRepository, String branchName, String remoteName) { - + verifyGitInstalled(); if ((remoteName == null) || remoteName.isEmpty()) { remoteName = DEFAULT_REMOTE; } @@ -255,7 +255,7 @@ public void reset(Path targetRepository, String branchName, String remoteName) { @Override public void cleanup(Path targetRepository) { - + verifyGitInstalled(); this.processContext.directory(targetRepository); ProcessResult result; // check for untracked files @@ -274,7 +274,7 @@ public void cleanup(Path targetRepository) { @Override public String retrieveGitUrl(Path repository) { - + verifyGitInstalled(); this.processContext.directory(repository); ProcessResult result; result = this.processContext.addArgs("-C", repository, "remote", "-v").run(ProcessMode.DEFAULT_CAPTURE); @@ -292,6 +292,20 @@ 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 IllegalStateException(message); + } + this.context.trace("Git is installed"); + } } From 9ad21dbc32ad98a61c81c81d4893649c21dc91d3 Mon Sep 17 00:00:00 2001 From: leonrohne27 Date: Fri, 29 Nov 2024 14:57:24 +0100 Subject: [PATCH 08/15] #737: Added cd command to shell commandlet (#748) --- CHANGELOG.adoc | 4 ++- .../tools/ide/commandlet/ShellCommandlet.java | 32 ++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 8cb546e92..3e2023a64 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -21,6 +21,7 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/637[#637]: Added option to disable updates * https://github.com/devonfw/IDEasy/issues/764[#764]: IDEasy not working properly in CMD * https://github.com/devonfw/IDEasy/issues/81[#81]: Implement Toolcommandlet for Kubernetes +* https://github.com/devonfw/IDEasy/issues/737[#737]: Adds cd command to ide shell * https://github.com/devonfw/IDEasy/issues/758[#758]: Create status commandlet * https://github.com/devonfw/IDEasy/issues/754[#754]: Again messages break processable command output @@ -33,7 +34,8 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/632[#632]: Add .editorconfig to settings workspaces * https://github.com/devonfw/IDEasy/issues/415[#415]: Added a message that will inform the user for what process he will need to enter his sudo-password * https://github.com/devonfw/IDEasy/issues/708[#708]: Open vscode in workspace path -* https://github.com/devonfw/IDEasy/issues/608[#608]: Enhanced error messages. Now logs missing command output and error messages +* https://github.com/devonfw/IDEasy/issues/608[#608]: Enhanced error messages. +Now logs missing command output and error messages * https://github.com/devonfw/IDEasy/issues/715[#715]: Show "cygwin is not supported" message for cygwin users * https://github.com/devonfw/IDEasy/issues/745[#745]: Maven install fails with NPE diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ShellCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ShellCommandlet.java index 6d4307297..a86301d52 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ShellCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ShellCommandlet.java @@ -1,6 +1,8 @@ package com.devonfw.tools.ide.commandlet; import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Iterator; import org.fusesource.jansi.AnsiConsole; @@ -79,13 +81,13 @@ public void run() { // TODO: implement TailTipWidgets, see: https://github.com/devonfw/IDEasy/issues/169 - String prompt = "ide> "; String rightPrompt = null; String line; AnsiConsole.systemInstall(); while (true) { try { + String prompt = context.getCwd() + "$ ide "; line = reader.readLine(prompt, rightPrompt, (MaskingCallback) null, null); line = line.trim(); if (line.equals("exit")) { @@ -129,9 +131,37 @@ private int runCommand(String args) { String[] arguments = args.split(" ", 0); CliArguments cliArgs = new CliArguments(arguments); cliArgs.next(); + + if ("cd".equals(arguments[0])) { + return changeDirectory(cliArgs); + } + return ((AbstractIdeContext) this.context).run(cliArgs); } + private int changeDirectory(CliArguments cliArgs) { + if (!cliArgs.hasNext()) { + Path homeDir = this.context.getUserHome(); + context.setCwd(homeDir, context.getWorkspaceName(), context.getIdeHome()); + return 0; + } + + String targetDir = String.valueOf(cliArgs.next()); + Path path = Paths.get(targetDir); + + // If the given path is relative, resolve it relative to the current directory + if (!path.isAbsolute()) { + path = context.getCwd().resolve(targetDir).normalize(); + } + + if (context.getFileAccess().isExpectedFolder(path)) { + context.setCwd(path, context.getWorkspaceName(), context.getIdeHome()); + return 0; + } else { + return 1; + } + } + /** * @param argument the current {@link CliArgument} (position) to match. * @param commandlet the potential {@link Commandlet} to match. From d7df00d12f9556e5f2d531e1f3199e2fd068690a Mon Sep 17 00:00:00 2001 From: leonrohne27 Date: Fri, 29 Nov 2024 17:30:58 +0100 Subject: [PATCH 09/15] #739: removed "not inside an IDE installation message" (#753) --- CHANGELOG.adoc | 1 + .../devonfw/tools/ide/cli/CliExitException.java | 17 +++++++++++++++++ .../ide/commandlet/EnvironmentCommandlet.java | 7 +++++-- .../tools/ide/context/AbstractIdeContext.java | 12 +----------- .../tools/ide/process/ProcessResult.java | 3 +++ .../commandlet/EnvironmentCommandletTest.java | 13 ------------- 6 files changed, 27 insertions(+), 26 deletions(-) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/cli/CliExitException.java diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 3e2023a64..56049c8dd 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -24,6 +24,7 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/737[#737]: Adds cd command to ide shell * https://github.com/devonfw/IDEasy/issues/758[#758]: Create status commandlet * https://github.com/devonfw/IDEasy/issues/754[#754]: Again messages break processable command output +* https://github.com/devonfw/IDEasy/issues/737[#739]: Improved error handling to show 'You are not inside an IDE installation' only when relevant. The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/16?closed=1[milestone 2024.12.001]. diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliExitException.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliExitException.java new file mode 100644 index 000000000..a33c6b3f0 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliExitException.java @@ -0,0 +1,17 @@ +package com.devonfw.tools.ide.cli; + +import com.devonfw.tools.ide.process.ProcessResult; + +/** + * {@link CliException} Empty exception that is thrown when a required variable is not set. + */ +public class CliExitException extends CliException { + + /** + * The constructor. + */ + public CliExitException() { + + super("", ProcessResult.EXIT); + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/EnvironmentCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/EnvironmentCommandlet.java index 79ef77972..6b2f7b019 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/EnvironmentCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/EnvironmentCommandlet.java @@ -4,6 +4,7 @@ import java.util.Map; import java.util.stream.Collectors; +import com.devonfw.tools.ide.cli.CliExitException; import com.devonfw.tools.ide.context.AbstractIdeContext; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.environment.EnvironmentVariablesType; @@ -42,7 +43,7 @@ public String getName() { @Override public boolean isIdeHomeRequired() { - return true; + return false; } @Override @@ -53,7 +54,9 @@ public boolean isProcessableOutput() { @Override public void run() { - + if (context.getIdeHome() == null) { + throw new CliExitException(); + } boolean winCmd = false; WindowsPathSyntax pathSyntax = null; IdeSubLogger logger = this.context.level(IdeLogLevel.PROCESSABLE); diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java index 28f8f0a7e..c66a3ba09 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java @@ -277,17 +277,6 @@ private String getMessageIdeRootNotFound() { } } - /** - * @return the status message about the {@link #getIdeHome() IDE_HOME} detection and environment variable initialization. - */ - public String getMessageIdeHome() { - - if (this.ideHome == null) { - return getMessageIdeHomeNotFound(); - } - return getMessageIdeHomeFound(); - } - /** * @return {@code true} if this is a test context for JUnits, {@code false} otherwise. */ @@ -301,6 +290,7 @@ protected SystemPath computeSystemPath() { return new SystemPath(this); } + private boolean isIdeHome(Path dir) { if (!Files.isDirectory(dir.resolve("workspaces"))) { 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..75d4e2460 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 @@ -24,6 +24,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. * 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 729039c87..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,7 +6,6 @@ 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.SystemInfoMock; @@ -127,18 +126,6 @@ public void testRunInfoLogging() { ); } - /** - * Test that {@link EnvironmentCommandlet} requires home. - */ - @Test - public void testThatHomeIsRequired() { - - // arrange - EnvironmentCommandlet env = new EnvironmentCommandlet(IdeTestContextMock.get()); - // act & assert - assertThat(env.isIdeHomeRequired()).isTrue(); - } - private String normalize(Path path) { return path.toString().replace('\\', '/'); From 886cea38611b7f5b2c097a7165c74f631c1d1509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Hohwiller?= Date: Mon, 2 Dec 2024 19:25:21 +0100 Subject: [PATCH 10/15] #824: fix git url with branch (#828) --- .../devonfw/tools/ide/cli/CliException.java | 6 +- .../tools/ide/cli/CliOfflineException.java | 3 +- .../tools/ide/cli/CliProcessException.java | 13 + .../commandlet/AbstractUpdateCommandlet.java | 5 +- .../ide/commandlet/RepositoryCommandlet.java | 4 +- .../ide/commandlet/RepositoryConfig.java | 14 + .../ide/commandlet/StatusCommandlet.java | 2 +- .../tools/ide/context/AbstractIdeContext.java | 5 +- .../devonfw/tools/ide/context/GitContext.java | 218 ------------ .../tools/ide/context/GitContextImpl.java | 311 ------------------ .../com/devonfw/tools/ide/context/GitUrl.java | 43 --- .../devonfw/tools/ide/context/IdeContext.java | 1 + .../com/devonfw/tools/ide/git/GitContext.java | 178 ++++++++++ .../devonfw/tools/ide/git/GitContextImpl.java | 294 +++++++++++++++++ .../ide/{context => git}/GitOperation.java | 26 +- .../com/devonfw/tools/ide/git/GitUrl.java | 79 +++++ .../ide/{context => git}/GitUrlSyntax.java | 2 +- .../tools/ide/process/ProcessContextImpl.java | 14 +- .../tools/ide/process/ProcessResult.java | 19 ++ .../tools/ide/process/ProcessResultImpl.java | 31 +- .../com/devonfw/tools/ide/tool/mvn/Mvn.java | 2 +- .../tools/ide/variable/IdeVariables.java | 2 +- .../tools/ide/context/GitContextMock.java | 92 ------ .../tools/ide/context/IdeTestContext.java | 2 + .../ide/context/ProcessContextGitMock.java | 7 +- .../devonfw/tools/ide/git/GitContextMock.java | 87 +++++ .../ide/{context => git}/GitContextTest.java | 23 +- .../{context => git}/GitOperationTest.java | 36 +- .../{context => git}/GitUrlSyntaxTest.java | 5 +- .../com/devonfw/tools/ide/git/GitUrlTest.java | 54 +++ .../tool/ide/IdeToolDummyCommandletTest.java | 2 +- 31 files changed, 860 insertions(+), 720 deletions(-) delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/context/GitContext.java delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/context/GitContextImpl.java delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/context/GitUrl.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java rename cli/src/main/java/com/devonfw/tools/ide/{context => git}/GitOperation.java (83%) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/git/GitUrl.java rename cli/src/main/java/com/devonfw/tools/ide/{context => git}/GitUrlSyntax.java (98%) delete mode 100644 cli/src/test/java/com/devonfw/tools/ide/context/GitContextMock.java create mode 100644 cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java rename cli/src/test/java/com/devonfw/tools/ide/{context => git}/GitContextTest.java (88%) rename cli/src/test/java/com/devonfw/tools/ide/{context => git}/GitOperationTest.java (85%) rename cli/src/test/java/com/devonfw/tools/ide/{context => git}/GitUrlSyntaxTest.java (94%) create mode 100644 cli/src/test/java/com/devonfw/tools/ide/git/GitUrlTest.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliException.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliException.java index 86defa648..16cb0598f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/cli/CliException.java +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliException.java @@ -1,5 +1,7 @@ package com.devonfw.tools.ide.cli; +import com.devonfw.tools.ide.process.ProcessResult; + /** * {@link RuntimeException} for to abort CLI process in expected situations. It allows to abort with a defined message for the end user and a defined exit code. * Unlike other exceptions a {@link CliException} is not treated as technical error. Therefore by default (unless in debug mode) no stacktrace is printed. @@ -37,8 +39,7 @@ public CliException(String message, Throwable cause) { */ public CliException(String message, int exitCode) { - super(message); - this.exitCode = exitCode; + this(message, exitCode, null); } /** @@ -51,6 +52,7 @@ public CliException(String message, int exitCode) { public CliException(String message, int exitCode, Throwable cause) { super(message, cause); + assert (exitCode != ProcessResult.SUCCESS); this.exitCode = exitCode; } diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliOfflineException.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliOfflineException.java index e288d420a..0baf1183e 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/cli/CliOfflineException.java +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliOfflineException.java @@ -1,6 +1,5 @@ package com.devonfw.tools.ide.cli; -import java.net.URL; import java.nio.file.Path; import com.devonfw.tools.ide.process.ProcessResult; @@ -67,7 +66,7 @@ public static CliOfflineException ofPurpose(String purpose) { * @param repository the path, where the repository should be cloned to. * @return A {@link CliOfflineException} with an informative message. */ - public static CliOfflineException ofClone(URL url, Path repository) { + public static CliOfflineException ofClone(String url, Path repository) { return new CliOfflineException("Could not clone " + url + " to " + repository + " because you are offline."); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/cli/CliProcessException.java b/cli/src/main/java/com/devonfw/tools/ide/cli/CliProcessException.java index 78c02e1d4..782fbf919 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/cli/CliProcessException.java +++ b/cli/src/main/java/com/devonfw/tools/ide/cli/CliProcessException.java @@ -13,6 +13,19 @@ public final class CliProcessException extends CliException { /** * The constructor. + * + * @param processResult the {@link #getProcessResult() process result}. + */ + public CliProcessException(ProcessResult processResult) { + + this("Command " + processResult.getExecutable() + " failed with exit code " + processResult.getExitCode() + " - full commandline was " + + processResult.getCommand(), processResult); + } + + /** + * The constructor. + * + * @param processResult the {@link #getProcessResult() process result}. */ public CliProcessException(String message, ProcessResult processResult) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java index a4f1d88a1..78df57524 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java @@ -7,8 +7,9 @@ import java.util.Set; import com.devonfw.tools.ide.context.AbstractIdeContext; -import com.devonfw.tools.ide.context.GitContext; import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.git.GitContext; +import com.devonfw.tools.ide.git.GitUrl; import com.devonfw.tools.ide.property.FlagProperty; import com.devonfw.tools.ide.property.StringProperty; import com.devonfw.tools.ide.repo.CustomTool; @@ -129,7 +130,7 @@ private void updateSettings() { } else if ("-".equals(repository)) { repository = IdeContext.DEFAULT_SETTINGS_REPO_URL; } - gitContext.pullOrClone(repository, settingsPath); + gitContext.pullOrClone(GitUrl.of(repository), settingsPath); } step.success("Successfully updated settings repository."); } finally { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryCommandlet.java index 770c9d76a..b7daa7e17 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryCommandlet.java @@ -83,7 +83,7 @@ private void doImportRepository(Path repositoryFile, boolean forceMode) { String repository = repositoryConfig.path(); String gitUrl = repositoryConfig.gitUrl(); - if (repository == null || "".equals(repository) || gitUrl == null || "".equals(gitUrl)) { + if (repository == null || repository.isEmpty() || gitUrl == null || gitUrl.isEmpty()) { this.context.warning("Invalid repository configuration {} - both 'path' and 'git-url' have to be defined.", repositoryFile.getFileName().toString()); return; } @@ -95,7 +95,7 @@ private void doImportRepository(Path repositoryFile, boolean forceMode) { this.context.getFileAccess().mkdirs(workspacePath); Path repositoryPath = workspacePath.resolve(repository); - this.context.getGitContext().pullOrClone(gitUrl, repositoryPath, repositoryConfig.gitBranch()); + this.context.getGitContext().pullOrClone(repositoryConfig.asGitUrl(), repositoryPath); String buildCmd = repositoryConfig.buildCmd(); this.context.debug("Building repository with ide command: {}", buildCmd); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryConfig.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryConfig.java index d05eb88aa..02b4183a6 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryConfig.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/RepositoryConfig.java @@ -8,6 +8,8 @@ import java.util.Properties; import java.util.Set; +import com.devonfw.tools.ide.git.GitUrl; + /** * Represents the configuration of a repository to be used by the {@link RepositoryCommandlet}. * @@ -32,6 +34,18 @@ public record RepositoryConfig( Set imports, boolean active) { + /** + * @return the {@link GitUrl} from {@link #gitUrl()} and {@link #gitBranch()}. + */ + public GitUrl asGitUrl() { + + return new GitUrl(this.gitUrl, this.gitBranch); + } + + /** + * @param filePath the {@link Path} to the {@link Properties} to load. + * @return the parsed {@link RepositoryConfig}. + */ public static RepositoryConfig loadProperties(Path filePath) { Properties properties = new Properties(); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java index 1513187fd..ea47bac6c 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java @@ -2,9 +2,9 @@ import java.nio.file.Path; -import com.devonfw.tools.ide.context.GitContext; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.environment.EnvironmentVariables; +import com.devonfw.tools.ide.git.GitContext; /** * {@link Commandlet} to print a status report about IDEasy. diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java index c66a3ba09..49a47bc11 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java @@ -30,6 +30,9 @@ import com.devonfw.tools.ide.environment.EnvironmentVariablesType; import com.devonfw.tools.ide.environment.IdeSystem; import com.devonfw.tools.ide.environment.IdeSystemImpl; +import com.devonfw.tools.ide.git.GitContext; +import com.devonfw.tools.ide.git.GitContextImpl; +import com.devonfw.tools.ide.git.GitUrl; import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.io.FileAccessImpl; import com.devonfw.tools.ide.log.IdeLogLevel; @@ -61,7 +64,7 @@ */ public abstract class AbstractIdeContext implements IdeContext { - private static final String IDE_URLS_GIT = "https://github.com/devonfw/ide-urls.git"; + private static final GitUrl IDE_URLS_GIT = new GitUrl("https://github.com/devonfw/ide-urls.git", null); private final IdeStartContextImpl startContext; 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 24afe7cfa..000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/context/GitContextImpl.java +++ /dev/null @@ -1,311 +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) { - verifyGitInstalled(); - 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) { - verifyGitInstalled(); - 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) { - verifyGitInstalled(); - 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) { - verifyGitInstalled(); - 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) { - verifyGitInstalled(); - 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) { - verifyGitInstalled(); - 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) { - verifyGitInstalled(); - 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) { - verifyGitInstalled(); - 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) { - verifyGitInstalled(); - 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; - } - - /** - * 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 IllegalStateException(message); - } - this.context.trace("Git is installed"); - } -} - - 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 787a38d64..af8076cf8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java @@ -11,6 +11,7 @@ import com.devonfw.tools.ide.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; 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..2e9e211e2 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java @@ -0,0 +1,294 @@ +package com.devonfw.tools.ide.git; + +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; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * 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", remote, 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 IllegalStateException(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 98% 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 2058082fe..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; 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 d0ab88f23..ed330fd01 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); @@ -188,7 +189,7 @@ public ProcessResult run(ProcessMode processMode) { exitCode = process.waitFor(); } - ProcessResult result = new ProcessResultImpl(exitCode, out, err); + ProcessResult result = new ProcessResultImpl(this.executable.getFileName().toString(), command, exitCode, out, err); performLogging(result, exitCode, interpreter); @@ -229,6 +230,17 @@ private static CompletableFuture> readInputStream(InputStream is) { }); } + 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 75d4e2460..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; @@ -41,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}. */ @@ -80,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 c4786d54b..4f09e9b51 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; @@ -21,18 +26,34 @@ 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}. */ - public ProcessResultImpl(int exitCode, List out, List err) { + 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()); } + @Override + public String getExecutable() { + + return this.executable; + } + + @Override + public String getCommand() { + + return this.command; + } + @Override public int getExitCode() { @@ -56,6 +77,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()) { @@ -76,4 +98,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/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/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/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/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/tool/ide/IdeToolDummyCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/ide/IdeToolDummyCommandletTest.java index 300806d7d..1bf582688 100644 --- 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 @@ -75,7 +75,7 @@ public ProcessResult runTool(ProcessMode processMode, GenericVersionRange toolVe // skip installation but trigger postInstall to test mocked plugin installation postInstall(true); - return new ProcessResultImpl(0, List.of(), List.of()); + return new ProcessResultImpl(this.tool, this.tool, 0, List.of(), List.of()); } @Override From 0183d3047a11beaa969cfe21aa8206c137a2cd48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Hohwiller?= Date: Tue, 3 Dec 2024 11:39:07 +0100 Subject: [PATCH 11/15] Update proxy-support.adoc: improved examples --- documentation/proxy-support.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/proxy-support.adoc b/documentation/proxy-support.adoc index 9d0570b5f..bd81fb3a6 100644 --- a/documentation/proxy-support.adoc +++ b/documentation/proxy-support.adoc @@ -31,7 +31,7 @@ as well as https://www.baeldung.com/java-custom-truststore[truststore settings] 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 +export IDE_OPTIONS="-Dhttps.proxyHost=proxy.host.com -Dhttps.proxyPort=8443" ``` === Authentication @@ -40,7 +40,7 @@ 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="-Dhttp.proxyUser=$USERNAME -Dhttp.proxyPassword=«password»" +export IDE_OPTIONS="-Dhttps.proxyUser=$USERNAME -Dhttps.proxyPassword=«password»" ``` === Truststore From f157554a74ba2309949ac5c3bef256cecc2e6fe8 Mon Sep 17 00:00:00 2001 From: jan-vcapgemini <59438728+jan-vcapgemini@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:44:17 +0100 Subject: [PATCH 12/15] #830: fixed missing version (#831) changed native-maven-image plugin phase from compile back to package --- .github/workflows/nightly-build.yml | 2 +- .github/workflows/release.yml | 2 +- cli/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 968bcc712..875e5b847 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -24,7 +24,7 @@ jobs: shell: bash run: | cd cli - mvn -B -ntp -Pnative -DskipTests=true compile + mvn -B -ntp -Pnative -DskipTests=true package - name: Upload native image uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3b5e1582b..a102e7d4e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,7 +31,7 @@ jobs: current_version="${current_version/ */}" next_version="${current_version/-SNAPSHOT/}" cd cli - mvn -B -ntp -Drevision=${next_version} -Pnative -DskipTests=true compile + mvn -B -ntp -Drevision=${next_version} -Pnative -DskipTests=true package - name: Upload native image uses: actions/upload-artifact@v4 with: diff --git a/cli/pom.xml b/cli/pom.xml index 5fc508d9a..9ca1d101d 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -226,7 +226,7 @@ compile-no-fork - compile + package test-native From f6948a007824607fa7d37842c3384a0bf020ebcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Hohwiller?= Date: Tue, 3 Dec 2024 13:02:23 +0100 Subject: [PATCH 13/15] Update settings.adoc: added repositories --- documentation/settings.adoc | 3 +++ 1 file changed, 3 insertions(+) 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] ---- From e4896cd39da17f27a80c3162f801f4a139722b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Hohwiller?= Date: Tue, 3 Dec 2024 13:11:24 +0100 Subject: [PATCH 14/15] Update repository.adoc: fixed AsciiDoc syntax --- documentation/repository.adoc | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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. From e849467f5321f044a0390b7f3b12e0ea3c07d519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Hohwiller?= Date: Tue, 3 Dec 2024 20:17:06 +0100 Subject: [PATCH 15/15] #824: added to CHANGELOG --- CHANGELOG.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 56049c8dd..4c67c3599 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -23,6 +23,7 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/81[#81]: Implement Toolcommandlet for Kubernetes * https://github.com/devonfw/IDEasy/issues/737[#737]: Adds cd command to ide shell * https://github.com/devonfw/IDEasy/issues/758[#758]: Create status commandlet +* https://github.com/devonfw/IDEasy/issues/824[#824]: ide create «settings-url»#«branch» not working * https://github.com/devonfw/IDEasy/issues/754[#754]: Again messages break processable command output * https://github.com/devonfw/IDEasy/issues/737[#739]: Improved error handling to show 'You are not inside an IDE installation' only when relevant.