• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devonfw / IDEasy / 26274501682

22 May 2026 07:27AM UTC coverage: 71.074% (+0.01%) from 71.062%
26274501682

Pull #1962

github

web-flow
Merge ed103790c into 58ae6752b
Pull Request #1962: #1255: Enhance snapshot version recognition in IDEasy

4480 of 6970 branches covered (64.28%)

Branch coverage included in aggregate %.

11535 of 15563 relevant lines covered (74.12%)

3.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.69
cli/src/main/java/com/devonfw/tools/ide/tool/IdeasyCommandlet.java
1
package com.devonfw.tools.ide.tool;
2

3
import java.io.Reader;
4
import java.io.Writer;
5
import java.nio.file.Files;
6
import java.nio.file.Path;
7
import java.util.ArrayList;
8
import java.util.Iterator;
9
import java.util.List;
10
import java.util.Map;
11
import java.util.Set;
12
import java.util.regex.Matcher;
13
import java.util.regex.Pattern;
14

15
import org.slf4j.Logger;
16
import org.slf4j.LoggerFactory;
17

18
import com.devonfw.tools.ide.cli.CliException;
19
import com.devonfw.tools.ide.commandlet.UpgradeMode;
20
import com.devonfw.tools.ide.common.SimpleSystemPath;
21
import com.devonfw.tools.ide.common.Tag;
22
import com.devonfw.tools.ide.context.IdeContext;
23
import com.devonfw.tools.ide.io.FileAccess;
24
import com.devonfw.tools.ide.io.ini.IniFile;
25
import com.devonfw.tools.ide.io.ini.IniSection;
26
import com.devonfw.tools.ide.log.IdeLogLevel;
27
import com.devonfw.tools.ide.os.WindowsHelper;
28
import com.devonfw.tools.ide.os.WindowsPathSyntax;
29
import com.devonfw.tools.ide.process.ProcessMode;
30
import com.devonfw.tools.ide.process.ProcessResult;
31
import com.devonfw.tools.ide.tool.mvn.MvnArtifact;
32
import com.devonfw.tools.ide.tool.mvn.MvnBasedLocalToolCommandlet;
33
import com.devonfw.tools.ide.tool.mvn.MvnRepository;
34
import com.devonfw.tools.ide.variable.IdeVariables;
35
import com.devonfw.tools.ide.version.IdeVersion;
36
import com.devonfw.tools.ide.version.VersionIdentifier;
37
import com.fasterxml.jackson.databind.JsonNode;
38
import com.fasterxml.jackson.databind.ObjectMapper;
39
import com.fasterxml.jackson.databind.node.ArrayNode;
40
import com.fasterxml.jackson.databind.node.ObjectNode;
41

42
/**
43
 * {@link MvnBasedLocalToolCommandlet} for IDEasy (ide-cli).
44
 */
45
public class IdeasyCommandlet extends MvnBasedLocalToolCommandlet {
46

47
  private static final Logger LOG = LoggerFactory.getLogger(IdeasyCommandlet.class);
3✔
48

49
  /** The {@link MvnArtifact} for IDEasy. */
50
  public static final MvnArtifact ARTIFACT = MvnArtifact.ofIdeasyCli("*!", "tar.gz", "${os}-${arch}");
5✔
51

52
  private static final String BASH_CODE_SOURCE_FUNCTIONS = "source \"$IDE_ROOT/_ide/installation/functions\"";
53

54
  /** The {@link #getName() tool name}. */
55
  public static final String TOOL_NAME = "ideasy";
56
  public static final String BASHRC = ".bashrc";
57
  public static final String ZSHRC = ".zshrc";
58
  public static final String IDE_BIN = "\\_ide\\bin";
59
  public static final String IDE_INSTALLATION_BIN = "\\_ide\\installation\\bin";
60

61

62
  private static final Map<String, Boolean> REQUIRED_INSTALLATION_ARTIFACTS = Map.of(
5✔
63
      //artifactName: String, required: boolean
64
      "bin", true,
3✔
65
      "functions", true,
3✔
66
      "internal", true,
3✔
67
      "gui", true,
3✔
68
      "system", true,
3✔
69
      "IDEasy.pdf", true,
3✔
70
      "setup", true,
3✔
71
      "setup.bat", false
1✔
72
  );
73

74
  private final UpgradeMode mode;
75

76
  /**
77
   * The constructor.
78
   *
79
   * @param context the {@link IdeContext}.
80
   */
81
  public IdeasyCommandlet(IdeContext context) {
82
    this(context, UpgradeMode.STABLE);
4✔
83
  }
1✔
84

85
  /**
86
   * The constructor.
87
   *
88
   * @param context the {@link IdeContext}.
89
   * @param mode the {@link UpgradeMode}.
90
   */
91
  public IdeasyCommandlet(IdeContext context, UpgradeMode mode) {
92

93
    super(context, TOOL_NAME, ARTIFACT, Set.of(Tag.PRODUCTIVITY, Tag.IDE));
8✔
94
    this.mode = mode;
3✔
95
  }
1✔
96

97
  @Override
98
  public VersionIdentifier getInstalledVersion() {
99

100
    return IdeVersion.getVersionIdentifier();
2✔
101
  }
102

103
  @Override
104
  public String getInstalledEdition() {
105

106
    return this.tool;
3✔
107
  }
108

109
  @Override
110
  public String getConfiguredEdition() {
111

112
    return this.tool;
3✔
113
  }
114

115
  @Override
116
  public VersionIdentifier getConfiguredVersion() {
117

118
    UpgradeMode upgradeMode = this.mode;
3✔
119
    if (upgradeMode == null) {
2!
120
      if (IdeVersion.isSnapshot()) {
2!
121
        upgradeMode = UpgradeMode.SNAPSHOT;
3✔
122
      } else {
123
        if (IdeVersion.getVersionIdentifier().getDevelopmentPhase().isStable()) {
×
124
          upgradeMode = UpgradeMode.STABLE;
×
125
        } else {
126
          upgradeMode = UpgradeMode.UNSTABLE;
×
127
        }
128
      }
129
    }
130
    return upgradeMode.getVersion();
3✔
131
  }
132

133
  @Override
134
  public Path getToolPath() {
135

136
    return this.context.getIdeInstallationPath();
4✔
137
  }
138

139
  @Override
140
  protected ToolInstallation doInstall(ToolInstallRequest request) {
141

142
    this.context.requireOnline("upgrade of IDEasy", true);
5✔
143

144
    if (IdeVersion.isUndefined() && !this.context.isForceMode()) {
6!
145
      VersionIdentifier version = IdeVersion.getVersionIdentifier();
2✔
146
      LOG.warn("You are using IDEasy version {} which indicates local development - skipping upgrade.", version);
4✔
147
      return toolAlreadyInstalled(request);
4✔
148
    }
149
    return super.doInstall(request);
×
150
  }
151

152
  /**
153
   * @return the latest released {@link VersionIdentifier version} of IDEasy.
154
   */
155
  public VersionIdentifier getLatestVersion() {
156

157
    if (!this.context.isForceMode()) {
4!
158
      VersionIdentifier currentVersion = IdeVersion.getVersionIdentifier();
2✔
159
      if (IdeVersion.isUndefined()) {
2!
160
        return currentVersion;
2✔
161
      }
162
    }
163
    VersionIdentifier configuredVersion = getConfiguredVersion();
×
164
    return getToolRepository().resolveVersion(this.tool, getConfiguredEdition(), configuredVersion, this);
×
165
  }
166

167
  /**
168
   * Checks if an update is available and logs according information.
169
   *
170
   * @return {@code true} if an update is available, {@code false} otherwise.
171
   */
172
  public boolean checkIfUpdateIsAvailable() {
173
    VersionIdentifier installedVersion = getInstalledVersion();
3✔
174
    IdeLogLevel.SUCCESS.log(LOG, "Your version of IDEasy is {}.", installedVersion);
10✔
175
    if (IdeVersion.isSnapshot()) {
2!
176
      LOG.warn("You are using a SNAPSHOT version of IDEasy. For stability consider switching to a stable release via 'ide upgrade --mode=stable'");
3✔
177
    }
178
    if (this.context.isOffline()) {
4✔
179
      LOG.warn("Skipping check for newer version of IDEasy because you are offline.");
3✔
180
      return false;
2✔
181
    }
182
    VersionIdentifier latestVersion = getLatestVersion();
3✔
183
    if (IdeVersion.isSnapshot()) {
2!
184
      if (isSameSnapshotVersion(installedVersion.toString(), latestVersion.toString())) {
7✔
185
        IdeLogLevel.SUCCESS.log(LOG, "Your are using the latest snapshot version of IDEasy and no update is available.");
4✔
186
        return false;
2✔
187
      }
188
    } else if (installedVersion.equals(latestVersion)) {
×
189
      IdeLogLevel.SUCCESS.log(LOG, "Your are using the latest stable version of IDEasy and no update is available.");
×
190
      return false;
×
191
    }
192
    IdeLogLevel.INTERACTION.log(LOG,
14✔
193
        "Your version of IDEasy is {} but version {} is available. Please run the following command to upgrade to the latest version:\n"
194
            + "ide upgrade", installedVersion, latestVersion);
195
    return true;
2✔
196
  }
197

198
  /**
199
   * Checks if two snapshot versions represent the version
200
   *
201
   * @param installed the installed version string
202
   * @param latest the latest available version string
203
   * @return {@code true} if both versions represent the same version, {@code false} otherwise.
204
   */
205
  private boolean isSameSnapshotVersion(String installed, String latest) {
206
    if (installed == null || latest == null) {
4!
207
      return false;
×
208
    }
209

210
    // base = before first '-'
211
    int dash_installed = installed.indexOf('-');
4✔
212
    int dash_latest = latest.indexOf('-');
4✔
213
    if (dash_installed <= 0 || dash_latest <= 0) {
4!
214
      return false;
2✔
215
    }
216

217
    String base_installed = installed.substring(0, dash_installed);
5✔
218
    String base_latest = latest.substring(0, dash_latest);
5✔
219

220
    if (!base_installed.equals(base_latest)) {
4✔
221
      return false;
2✔
222
    }
223

224
    // extract base year (YYYY)
225
    Matcher baseMatcher_installed = Pattern.compile("^(\\d{4})\\.\\d{2}\\.\\d{3}$").matcher(base_installed);
5✔
226
    Matcher baseMatcher_latest = Pattern.compile("^(\\d{4})\\.\\d{2}\\.\\d{3}$").matcher(base_latest);
5✔
227
    if (!baseMatcher_installed.matches() || !baseMatcher_latest.matches()) {
6!
228
      return false;
×
229
    }
230

231
    String baseYear_installed = baseMatcher_installed.group(1);
4✔
232
    Matcher installedMatcher = Pattern.compile("^" + Pattern.quote(base_installed) + "-(\\d{2})_(\\d{2})_(\\d{2}).*-SNAPSHOT$")
5✔
233
        .matcher(installed);
2✔
234
    if (!installedMatcher.matches()) {
3!
235
      return false;
×
236
    }
237
    String keyInstalled = installedMatcher.group(1) + installedMatcher.group(2) + "." + installedMatcher.group(3); // MMDD.HH
11✔
238

239
    Matcher latestMatcher = Pattern.compile("^" + Pattern.quote(base_installed) + "-(\\d{4})(\\d{2})(\\d{2})\\.(\\d{2})\\d{4}.*$")
5✔
240
        .matcher(latest);
2✔
241
    if (!latestMatcher.matches()) {
3!
242
      return false;
×
243
    }
244

245
    String latestYear = latestMatcher.group(1);
4✔
246
    if (!baseYear_installed.equals(latestYear)) {
4!
247
      return false;
×
248
    }
249

250
    String keyLatest = latestMatcher.group(2) + latestMatcher.group(3) + "." + latestMatcher.group(4); // MMDD.HH
11✔
251

252
    return keyInstalled.equals(keyLatest);
4✔
253
  }
254

255
  /**
256
   * Initial installation of IDEasy.
257
   *
258
   * @param cwd the {@link Path} to the current working directory.
259
   * @see com.devonfw.tools.ide.commandlet.InstallCommandlet
260
   */
261
  public void installIdeasy(Path cwd) {
262
    Path ideRoot = determineIdeRoot(cwd);
4✔
263
    Path idePath = ideRoot.resolve(IdeContext.FOLDER_UNDERSCORE_IDE);
4✔
264
    Path installationPath = idePath.resolve(IdeContext.FOLDER_INSTALLATION);
4✔
265
    Path ideasySoftwarePath = idePath.resolve(IdeContext.FOLDER_SOFTWARE).resolve(MvnRepository.ID).resolve(IdeasyCommandlet.TOOL_NAME)
8✔
266
        .resolve(IdeasyCommandlet.TOOL_NAME);
2✔
267
    Path ideasyVersionPath = ideasySoftwarePath.resolve(IdeVersion.getVersionString());
4✔
268
    FileAccess fileAccess = this.context.getFileAccess();
4✔
269
    if (Files.isDirectory(ideasyVersionPath)) {
5✔
270
      LOG.error("IDEasy is already installed at {} - if your installation is broken, delete it manually and rerun setup!", ideasyVersionPath);
5✔
271
    } else {
272
      List<Path> installationArtifacts = new ArrayList<>();
4✔
273
      for (Map.Entry<String, Boolean> artifactEntry : REQUIRED_INSTALLATION_ARTIFACTS.entrySet()) {
11✔
274
        String artifactName = artifactEntry.getKey();
4✔
275
        boolean required = artifactEntry.getValue();
5✔
276
        boolean success = addInstallationArtifact(cwd, artifactName, required, installationArtifacts);
7✔
277
        if (!success) {
2!
278
          throw new CliException("IDEasy release is inconsistent at %s [artifact=%s]".formatted(cwd, artifactName));
×
279
        }
280
      }
1✔
281

282
      fileAccess.mkdirs(ideasyVersionPath);
3✔
283
      for (Path installationArtifact : installationArtifacts) {
10✔
284
        fileAccess.copy(installationArtifact, ideasyVersionPath);
4✔
285
      }
1✔
286
      this.context.writeVersionFile(IdeVersion.getVersionIdentifier(), ideasyVersionPath);
5✔
287
    }
288
    fileAccess.symlink(ideasyVersionPath, installationPath);
4✔
289
    addToShellRc(BASHRC, ideRoot, null);
5✔
290
    addToShellRc(ZSHRC, ideRoot, "autoload -U +X bashcompinit && bashcompinit");
5✔
291
    installIdeasyWindowsEnv(ideRoot, installationPath);
4✔
292
    IdeLogLevel.SUCCESS.log(LOG, "IDEasy has been installed successfully on your system.");
4✔
293
    LOG.warn("IDEasy has been setup for new shells but it cannot work in your current shell(s).\n"
3✔
294
        + "To use it here, run 'source ~/.bashrc' (or your shell config). Otherwise, open a new terminal or reboot.");
295
  }
1✔
296

297
  private void installIdeasyWindowsEnv(Path ideRoot, Path installationPath) {
298
    if (!this.context.getSystemInfo().isWindows()) {
5✔
299
      return;
1✔
300
    }
301
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
302
    helper.setUserEnvironmentValue(IdeVariables.IDE_ROOT.getName(), ideRoot.toString());
6✔
303
    String userPath = helper.getUserEnvironmentValue(IdeVariables.PATH.getName());
5✔
304
    if (userPath == null) {
2!
305
      LOG.error("Could not read user PATH from registry!");
×
306
    } else {
307
      LOG.info("Found user PATH={}", userPath);
4✔
308
      Path ideasyBinPath = installationPath.resolve("bin");
4✔
309
      SimpleSystemPath path = SimpleSystemPath.of(userPath, ';');
4✔
310
      if (path.getEntries().isEmpty()) {
4!
311
        LOG.warn("ATTENTION:\n"
×
312
            + "Your user specific PATH variable seems to be empty.\n"
313
            + "You can double check this by pressing [Windows][r] and launch the program SystemPropertiesAdvanced.\n"
314
            + "Then click on 'Environment variables' and check if 'PATH' is set in the 'user variables' from the upper list.\n"
315
            + "In case 'PATH' is defined there non-empty and you get this message, please abort and give us feedback:\n"
316
            + "https://github.com/devonfw/IDEasy/issues\n"
317
            + "Otherwise all is correct and you can continue.");
318
        this.context.askToContinue("Are you sure you want to override your PATH?");
×
319
      } else {
320
        path.removeEntries(s -> s.endsWith(IDE_INSTALLATION_BIN));
7✔
321
      }
322
      path.getEntries().add(ideasyBinPath.toString());
6✔
323
      helper.setUserEnvironmentValue(IdeVariables.PATH.getName(), path.toString());
6✔
324
      setGitLongpaths();
2✔
325
    }
326
  }
1✔
327

328
  private void setGitLongpaths() {
329
    this.context.getGitContext().findGitRequired();
5✔
330
    Path configPath = this.context.getUserHome().resolve(".gitconfig");
6✔
331
    FileAccess fileAccess = this.context.getFileAccess();
4✔
332
    IniFile iniFile = fileAccess.readIniFile(configPath);
4✔
333
    IniSection coreSection = iniFile.getOrCreateSection("core");
4✔
334
    coreSection.setProperty("longpaths", "true");
4✔
335
    fileAccess.writeIniFile(iniFile, configPath);
4✔
336
  }
1✔
337

338
  /**
339
   * Sets up Windows Terminal with Git Bash integration.
340
   */
341
  public void setupWindowsTerminal() {
342
    if (!this.context.getSystemInfo().isWindows()) {
×
343
      return;
×
344
    }
345
    if (!isWindowsTerminalInstalled()) {
×
346
      try {
347
        installWindowsTerminal();
×
348
      } catch (Exception e) {
×
349
        LOG.error("Failed to install Windows Terminal!", e);
×
350
      }
×
351
    }
352
    configureWindowsTerminalGitBash();
×
353
  }
×
354

355
  /**
356
   * Checks if Windows Terminal is installed.
357
   *
358
   * @return {@code true} if Windows Terminal is installed, {@code false} otherwise.
359
   */
360
  private boolean isWindowsTerminalInstalled() {
361
    try {
362
      ProcessResult result = this.context.newProcess()
×
363
          .executable("powershell")
×
364
          .addArgs("-Command", "Get-AppxPackage -Name Microsoft.WindowsTerminal")
×
365
          .run(ProcessMode.DEFAULT_CAPTURE);
×
366
      return result.isSuccessful() && !result.getOut().isEmpty();
×
367
    } catch (Exception e) {
×
368
      LOG.debug("Failed to check Windows Terminal installation.", e);
×
369
      return false;
×
370
    }
371
  }
372

373
  /**
374
   * Installs Windows Terminal using winget.
375
   */
376
  private void installWindowsTerminal() {
377
    try {
378
      LOG.info("Installing Windows Terminal...");
×
379
      ProcessResult result = this.context.newProcess()
×
380
          .executable("winget")
×
381
          .addArgs("install", "Microsoft.WindowsTerminal")
×
382
          .run(ProcessMode.DEFAULT);
×
383
      if (result.isSuccessful()) {
×
384
        IdeLogLevel.SUCCESS.log(LOG, "Windows Terminal has been installed successfully.");
×
385
      } else {
386
        LOG.warn("Failed to install Windows Terminal. Please install it manually from Microsoft Store.");
×
387
      }
388
    } catch (Exception e) {
×
389
      LOG.warn("Failed to install Windows Terminal: {}. Please install it manually from Microsoft Store.", e.toString());
×
390
    }
×
391
  }
×
392

393
  /**
394
   * Configures Git Bash integration in Windows Terminal.
395
   */
396
  protected void configureWindowsTerminalGitBash() {
397
    Path settingsPath = getWindowsTerminalSettingsPath();
3✔
398
    if (settingsPath == null || !Files.exists(settingsPath)) {
7!
399
      LOG.warn("Windows Terminal settings file not found. Cannot configure Git Bash integration.");
×
400
      return;
×
401
    }
402

403
    try {
404
      Path bashPath = this.context.findBash();
4✔
405
      if (bashPath == null) {
2!
406
        LOG.warn("Git Bash not found. Cannot configure Windows Terminal integration.");
×
407
        return;
×
408
      }
409

410
      configureGitBashProfile(settingsPath, bashPath.toString());
5✔
411
      IdeLogLevel.SUCCESS.log(LOG, "Git Bash has been configured in Windows Terminal.");
4✔
412
    } catch (Exception e) {
×
413
      LOG.warn("Failed to configure Git Bash in Windows Terminal: {}", e.getMessage());
×
414
    }
1✔
415
  }
1✔
416

417
  /**
418
   * Gets the Windows Terminal settings file path.
419
   *
420
   * @return the {@link Path} to the Windows Terminal settings file, or {@code null} if not found.
421
   */
422
  protected Path getWindowsTerminalSettingsPath() {
423
    Path localAppData = this.context.getUserHome().resolve("AppData").resolve("Local");
8✔
424
    Path packagesPath = localAppData.resolve("Packages");
4✔
425

426
    // Try the new Windows Terminal package first
427
    Path newTerminalPath = packagesPath.resolve("Microsoft.WindowsTerminal_8wekyb3d8bbwe")
4✔
428
        .resolve("LocalState").resolve("settings.json");
4✔
429
    if (Files.exists(newTerminalPath)) {
5!
430
      return newTerminalPath;
2✔
431
    }
432

433
    // Try the old Windows Terminal Preview package
434
    Path previewPath = packagesPath.resolve("Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe")
×
435
        .resolve("LocalState").resolve("settings.json");
×
436
    if (Files.exists(previewPath)) {
×
437
      return previewPath;
×
438
    }
439

440
    return null;
×
441
  }
442

443
  /**
444
   * Configures Git Bash profile in Windows Terminal settings.
445
   *
446
   * @param settingsPath the {@link Path} to the Windows Terminal settings file.
447
   * @param bashPath the path to the Git Bash executable.
448
   */
449
  private void configureGitBashProfile(Path settingsPath, String bashPath) throws Exception {
450

451
    ObjectMapper mapper = new ObjectMapper();
4✔
452
    ObjectNode root;
453
    try (Reader reader = Files.newBufferedReader(settingsPath)) {
3✔
454
      JsonNode rootNode = mapper.readTree(reader);
4✔
455
      root = (ObjectNode) rootNode;
3✔
456
    }
457

458
    // Get or create profiles object
459
    ObjectNode profiles = (ObjectNode) root.get("profiles");
5✔
460
    if (profiles == null) {
2!
461
      profiles = mapper.createObjectNode();
×
462
      root.set("profiles", profiles);
×
463
    }
464

465
    // Get or create list array of profiles object
466
    JsonNode profilesList = profiles.get("list");
4✔
467
    if (profilesList == null || !profilesList.isArray()) {
5!
468
      profilesList = mapper.createArrayNode();
×
469
      profiles.set("list", profilesList);
×
470
    }
471

472
    // Check if Git Bash profile already exists
473
    boolean gitBashProfileExists = false;
2✔
474
    for (JsonNode profile : profilesList) {
10✔
475
      if (profile.has("name") && profile.get("name").asText().equals("Git Bash")) {
11!
476
        gitBashProfileExists = true;
×
477
        break;
×
478
      }
479
    }
1✔
480

481
    // Add Git Bash profile if it doesn't exist
482
    if (gitBashProfileExists) {
2!
483
      LOG.info("Git Bash profile already exists in {}.", settingsPath);
×
484
    } else {
485
      ObjectNode gitBashProfile = mapper.createObjectNode();
3✔
486
      String newGuid = "{2ece5bfe-50ed-5f3a-ab87-5cd4baafed2b}";
2✔
487
      String iconPath = getGitBashIconPath(bashPath);
4✔
488
      String startingDirectory = this.context.getIdeRoot().toString();
5✔
489

490
      gitBashProfile.put("guid", newGuid);
5✔
491
      gitBashProfile.put("name", "Git Bash");
5✔
492
      gitBashProfile.put("commandline", bashPath);
5✔
493
      gitBashProfile.put("icon", iconPath);
5✔
494
      gitBashProfile.put("startingDirectory", startingDirectory);
5✔
495

496
      ((ArrayNode) profilesList).add(gitBashProfile);
5✔
497

498
      // Set Git Bash as default profile
499
      root.put("defaultProfile", newGuid);
5✔
500

501
      // Write back to file
502
      try (Writer writer = Files.newBufferedWriter(settingsPath)) {
5✔
503
        mapper.writerWithDefaultPrettyPrinter().writeValue(writer, root);
5✔
504
      }
505
    }
506
  }
1✔
507

508
  private String getGitBashIconPath(String bashPathString) {
509
    Path bashPath = Path.of(bashPathString);
5✔
510
    // "C:\\Program Files\\Git\\bin\\bash.exe"
511
    // "C:\\Program Files\\Git\\mingw64\\share\\git\\git-for-windows.ico"
512
    Path parent = bashPath.getParent();
3✔
513
    if (parent != null) {
2!
514
      Path iconPath = parent.resolve("mingw64/share/git/git-for-windows.ico");
4✔
515
      if (Files.exists(iconPath)) {
5!
516
        LOG.debug("Found git-bash icon at {}", iconPath);
×
517
        return iconPath.toString();
×
518
      }
519
      LOG.debug("Git Bash icon not found at {}. Using default icon.", iconPath);
4✔
520
    }
521
    return "ms-appx:///ProfileIcons/{0caa0dad-35be-5f56-a8ff-afceeeaa6101}.png";
2✔
522
  }
523

524
  static String removeObsoleteEntryFromWindowsPath(String userPath) {
525
    return removeEntryFromWindowsPath(userPath, IDE_BIN);
4✔
526
  }
527

528
  static String removeEntryFromWindowsPath(String userPath, String suffix) {
529
    int len = userPath.length();
3✔
530
    int start = 0;
2✔
531
    while ((start >= 0) && (start < len)) {
5!
532
      int end = userPath.indexOf(';', start);
5✔
533
      if (end < 0) {
2✔
534
        end = len;
2✔
535
      }
536
      String entry = userPath.substring(start, end);
5✔
537
      if (entry.endsWith(suffix)) {
4✔
538
        String prefix = "";
2✔
539
        int offset = 1;
2✔
540
        if (start > 0) {
2✔
541
          prefix = userPath.substring(0, start - 1);
7✔
542
          offset = 0;
2✔
543
        }
544
        if (end == len) {
3✔
545
          return prefix;
2✔
546
        } else {
547
          return removeEntryFromWindowsPath(prefix + userPath.substring(end + offset), suffix);
10✔
548
        }
549
      }
550
      start = end + 1;
4✔
551
    }
1✔
552
    return userPath;
2✔
553
  }
554

555
  /**
556
   * Adds ourselves to the shell RC (run-commands) configuration file.
557
   *
558
   * @param filename the name of the RC file.
559
   * @param ideRoot the IDE_ROOT {@link Path}.
560
   */
561
  private void addToShellRc(String filename, Path ideRoot, String extraLine) {
562

563
    modifyShellRc(filename, ideRoot, true, extraLine);
6✔
564
  }
1✔
565

566
  private void removeFromShellRc(String filename, Path ideRoot) {
567

568
    modifyShellRc(filename, ideRoot, false, null);
6✔
569
  }
1✔
570

571
  /**
572
   * Adds ourselves to the shell RC (run-commands) configuration file.
573
   *
574
   * @param filename the name of the RC file.
575
   * @param ideRoot the IDE_ROOT {@link Path}.
576
   */
577
  private void modifyShellRc(String filename, Path ideRoot, boolean add, String extraLine) {
578

579
    if (add) {
2✔
580
      LOG.info("Configuring IDEasy in {}", filename);
5✔
581
    } else {
582
      LOG.info("Removing IDEasy from {}", filename);
4✔
583
    }
584
    Path rcFile = this.context.getUserHome().resolve(filename);
6✔
585
    FileAccess fileAccess = this.context.getFileAccess();
4✔
586
    List<String> lines = fileAccess.readFileLines(rcFile);
4✔
587
    if (lines == null) {
2✔
588
      if (!add) {
2!
589
        return;
×
590
      }
591
      lines = new ArrayList<>();
5✔
592
    } else {
593
      // since it is unspecified if the returned List may be immutable we want to get sure
594
      lines = new ArrayList<>(lines);
5✔
595
    }
596
    Iterator<String> iterator = lines.iterator();
3✔
597
    int removeCount = 0;
2✔
598
    while (iterator.hasNext()) {
3✔
599
      String line = iterator.next();
4✔
600
      line = line.trim();
3✔
601
      if (isObsoleteRcLine(line)) {
3✔
602
        LOG.info("Removing obsolete line from {}: {}", filename, line);
5✔
603
        iterator.remove();
2✔
604
        removeCount++;
2✔
605
      } else if (line.equals(extraLine)) {
4✔
606
        extraLine = null;
2✔
607
      }
608
    }
1✔
609
    if (add) {
2✔
610
      if (extraLine != null) {
2!
611
        lines.add(extraLine);
×
612
      }
613
      if (!this.context.getSystemInfo().isWindows()) {
5✔
614
        lines.add("export IDE_ROOT=\"" + WindowsPathSyntax.MSYS.format(ideRoot) + "\"");
7✔
615
      }
616
      lines.add(BASH_CODE_SOURCE_FUNCTIONS);
4✔
617
    }
618
    fileAccess.writeFileLines(lines, rcFile);
4✔
619
    LOG.debug("Successfully updated {}", filename);
4✔
620
  }
1✔
621

622
  private static boolean isObsoleteRcLine(String line) {
623
    if (line.startsWith("alias ide=")) {
4!
624
      return true;
×
625
    } else if (line.startsWith("export IDE_ROOT=")) {
4!
626
      return true;
×
627
    } else if (line.equals("ide")) {
4✔
628
      return true;
2✔
629
    } else if (line.equals("ide init")) {
4✔
630
      return true;
2✔
631
    } else if (line.startsWith("source \"$IDE_ROOT/_ide/")) {
4✔
632
      return true;
2✔
633
    }
634
    return false;
2✔
635
  }
636

637
  private boolean addInstallationArtifact(Path cwd, String artifactName, boolean required, List<Path> installationArtifacts) {
638

639
    Path artifactPath = cwd.resolve(artifactName);
4✔
640
    if (Files.exists(artifactPath)) {
5!
641
      installationArtifacts.add(artifactPath);
5✔
642
    } else if (required) {
×
643
      LOG.error("Missing required file {}", artifactName);
×
644
      return false;
×
645
    }
646
    return true;
2✔
647
  }
648

649
  private Path determineIdeRoot(Path cwd) {
650
    Path ideRoot = this.context.getIdeRoot();
4✔
651
    if (ideRoot == null) {
2!
652
      Path home = this.context.getUserHome();
4✔
653
      Path installRoot = home;
2✔
654
      if (this.context.getSystemInfo().isWindows()) {
5✔
655
        if (!cwd.startsWith(home)) {
4!
656
          installRoot = cwd.getRoot();
×
657
        }
658
      }
659
      ideRoot = installRoot.resolve(IdeContext.FOLDER_PROJECTS);
4✔
660
    } else {
1✔
661
      assert (Files.isDirectory(ideRoot)) : "IDE_ROOT directory does not exist!";
×
662
    }
663
    return ideRoot;
2✔
664
  }
665

666
  /**
667
   * Uninstalls IDEasy entirely from the system.
668
   */
669
  public void uninstallIdeasy() {
670

671
    Path ideRoot = this.context.getIdeRoot();
4✔
672
    removeFromShellRc(BASHRC, ideRoot);
4✔
673
    removeFromShellRc(ZSHRC, ideRoot);
4✔
674
    Path idePath = this.context.getIdePath();
4✔
675
    uninstallIdeasyWindowsEnv(ideRoot);
3✔
676
    uninstallIdeasyIdePath(idePath);
3✔
677
    deleteDownloadCache();
2✔
678
    IdeLogLevel.SUCCESS.log(LOG, "IDEasy has been uninstalled from your system.");
4✔
679
    IdeLogLevel.INTERACTION.log(LOG, "ATTENTION:\n"
10✔
680
        + "In order to prevent data-loss, we do not delete your projects and git repositories!\n"
681
        + "To entirely get rid of IDEasy, also check your IDE_ROOT folder at:\n"
682
        + "{}", ideRoot);
683
  }
1✔
684

685
  private void deleteDownloadCache() {
686
    Path downloadPath = this.context.getDownloadPath();
4✔
687
    LOG.info("Deleting download cache from {}", downloadPath);
4✔
688
    this.context.getFileAccess().delete(downloadPath);
5✔
689
  }
1✔
690

691
  private void uninstallIdeasyIdePath(Path idePath) {
692
    if (this.context.getSystemInfo().isWindows()) {
5✔
693
      Path bash = this.context.findBash();
4✔
694
      if (bash == null) {
2!
695
        LOG.warn("Could not find bash for asynchronous deletion of {}. Falling back to direct deletion.", idePath);
×
696
        this.context.getFileAccess().delete(idePath);
×
697
        return;
×
698
      }
699
      this.context.newProcess().executable(bash).addArgs("-c",
17✔
700
          "sleep 10 && rm -rf \"" + WindowsPathSyntax.MSYS.format(idePath) + "\"").run(ProcessMode.BACKGROUND);
5✔
701
      IdeLogLevel.INTERACTION.log(LOG,
10✔
702
          "To prevent windows file locking errors, we perform an asynchronous deletion of {} in background now.\n"
703
              + "Please close all terminals and wait a minute for the deletion to complete before running other commands.", idePath);
704
    } else {
1✔
705
      LOG.info("Finally deleting {}", idePath);
4✔
706
      this.context.getFileAccess().delete(idePath);
5✔
707
    }
708
  }
1✔
709

710
  private void uninstallIdeasyWindowsEnv(Path ideRoot) {
711
    if (!this.context.getSystemInfo().isWindows()) {
5✔
712
      return;
1✔
713
    }
714
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
715
    helper.removeUserEnvironmentValue(IdeVariables.IDE_ROOT.getName());
4✔
716
    String userPath = helper.getUserEnvironmentValue(IdeVariables.PATH.getName());
5✔
717
    if (userPath == null) {
2!
718
      LOG.error("Could not read user PATH from registry!");
×
719
    } else {
720
      LOG.info("Found user PATH={}", userPath);
4✔
721
      String newUserPath = userPath;
2✔
722
      if (!userPath.isEmpty()) {
3!
723
        SimpleSystemPath path = SimpleSystemPath.of(userPath, ';');
4✔
724
        path.removeEntries(s -> s.endsWith(IDE_BIN) || s.endsWith(IDE_INSTALLATION_BIN));
15!
725
        newUserPath = path.toString();
3✔
726
      }
727
      if (newUserPath.equals(userPath)) {
4!
728
        LOG.error("Could not find IDEasy in PATH:\n{}", userPath);
×
729
      } else {
730
        helper.setUserEnvironmentValue(IdeVariables.PATH.getName(), newUserPath);
5✔
731
      }
732
    }
733
  }
1✔
734
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc