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

devonfw / IDEasy / 26273115222

22 May 2026 06:53AM UTC coverage: 71.066% (+0.004%) from 71.062%
26273115222

Pull #1962

github

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

4481 of 6972 branches covered (64.27%)

Branch coverage included in aggregate %.

11533 of 15562 relevant lines covered (74.11%)

3.14 hits per line

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

73.12
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() && isSameSnapshotVersion(installedVersion.toString(), latestVersion.toString())) {
9!
184
      IdeLogLevel.SUCCESS.log(LOG, "Your are using the latest Snapshot version of IDEasy and no update is available.");
4✔
185
      return false;
2✔
186
    } else if (installedVersion.equals(latestVersion) && !IdeVersion.isSnapshot()) {
6!
187
      IdeLogLevel.SUCCESS.log(LOG, "Your are using the latest Stable version of IDEasy and no update is available.");
×
188
      return false;
×
189
    } else {
190
      IdeLogLevel.INTERACTION.log(LOG,
14✔
191
          "Your version of IDEasy is {} but version {} is available. Please run the following command to upgrade to the latest version:\n"
192
              + "ide upgrade", installedVersion, latestVersion);
193
      return true;
2✔
194
    }
195
  }
196

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

439
    return null;
×
440
  }
441

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

709
  private void uninstallIdeasyWindowsEnv(Path ideRoot) {
710
    if (!this.context.getSystemInfo().isWindows()) {
5✔
711
      return;
1✔
712
    }
713
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
714
    helper.removeUserEnvironmentValue(IdeVariables.IDE_ROOT.getName());
4✔
715
    String userPath = helper.getUserEnvironmentValue(IdeVariables.PATH.getName());
5✔
716
    if (userPath == null) {
2!
717
      LOG.error("Could not read user PATH from registry!");
×
718
    } else {
719
      LOG.info("Found user PATH={}", userPath);
4✔
720
      String newUserPath = userPath;
2✔
721
      if (!userPath.isEmpty()) {
3!
722
        SimpleSystemPath path = SimpleSystemPath.of(userPath, ';');
4✔
723
        path.removeEntries(s -> s.endsWith(IDE_BIN) || s.endsWith(IDE_INSTALLATION_BIN));
15!
724
        newUserPath = path.toString();
3✔
725
      }
726
      if (newUserPath.equals(userPath)) {
4!
727
        LOG.error("Could not find IDEasy in PATH:\n{}", userPath);
×
728
      } else {
729
        helper.setUserEnvironmentValue(IdeVariables.PATH.getName(), newUserPath);
5✔
730
      }
731
    }
732
  }
1✔
733
}
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