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

devonfw / IDEasy / 15827486357

23 Jun 2025 02:46PM UTC coverage: 67.803% (+0.01%) from 67.793%
15827486357

Pull #1381

github

web-flow
Merge e663dfa8d into f0c645132
Pull Request #1381: #1346: Fix ide upgrade throwing exception when offline

3179 of 5090 branches covered (62.46%)

Branch coverage included in aggregate %.

8140 of 11604 relevant lines covered (70.15%)

3.08 hits per line

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

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

3
import java.io.IOException;
4
import java.nio.file.Files;
5
import java.nio.file.Path;
6
import java.util.ArrayList;
7
import java.util.Iterator;
8
import java.util.LinkedHashMap;
9
import java.util.List;
10
import java.util.Set;
11

12
import com.devonfw.tools.ide.cli.CliException;
13
import com.devonfw.tools.ide.commandlet.UpgradeMode;
14
import com.devonfw.tools.ide.common.SimpleSystemPath;
15
import com.devonfw.tools.ide.common.Tag;
16
import com.devonfw.tools.ide.context.IdeContext;
17
import com.devonfw.tools.ide.git.GitContext;
18
import com.devonfw.tools.ide.git.GitContextImpl;
19
import com.devonfw.tools.ide.io.FileAccess;
20
import com.devonfw.tools.ide.os.WindowsHelper;
21
import com.devonfw.tools.ide.os.WindowsPathSyntax;
22
import com.devonfw.tools.ide.process.ProcessMode;
23
import com.devonfw.tools.ide.tool.mvn.MvnArtifact;
24
import com.devonfw.tools.ide.tool.mvn.MvnBasedLocalToolCommandlet;
25
import com.devonfw.tools.ide.tool.repository.MavenRepository;
26
import com.devonfw.tools.ide.variable.IdeVariables;
27
import com.devonfw.tools.ide.version.IdeVersion;
28
import com.devonfw.tools.ide.version.VersionIdentifier;
29

30
/**
31
 * {@link MvnBasedLocalToolCommandlet} for IDEasy (ide-cli).
32
 */
33
public class IdeasyCommandlet extends MvnBasedLocalToolCommandlet {
34

35
  /** The {@link MvnArtifact} for IDEasy. */
36
  public static final MvnArtifact ARTIFACT = MvnArtifact.ofIdeasyCli("*!", "tar.gz", "${os}-${arch}");
6✔
37

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

40
  /** The {@link #getName() tool name}. */
41
  public static final String TOOL_NAME = "ideasy";
42
  public static final String BASHRC = ".bashrc";
43
  public static final String ZSHRC = ".zshrc";
44
  public static final String IDE_BIN = "\\_ide\\bin";
45
  public static final String IDE_INSTALLATION_BIN = "\\_ide\\installation\\bin";
46

47
  private final UpgradeMode mode;
48

49
  /**
50
   * The constructor.
51
   *
52
   * @param context the {@link IdeContext}.
53
   */
54
  public IdeasyCommandlet(IdeContext context) {
55
    this(context, UpgradeMode.STABLE);
4✔
56
  }
1✔
57

58
  /**
59
   * The constructor.
60
   *
61
   * @param context the {@link IdeContext}.
62
   * @param mode the {@link UpgradeMode}.
63
   */
64
  public IdeasyCommandlet(IdeContext context, UpgradeMode mode) {
65

66
    super(context, TOOL_NAME, ARTIFACT, Set.of(Tag.PRODUCTIVITY, Tag.IDE));
8✔
67
    this.mode = mode;
3✔
68
  }
1✔
69

70
  @Override
71
  public VersionIdentifier getInstalledVersion() {
72

73
    return IdeVersion.getVersionIdentifier();
2✔
74
  }
75

76
  @Override
77
  public String getConfiguredEdition() {
78

79
    return this.tool;
×
80
  }
81

82
  @Override
83
  public VersionIdentifier getConfiguredVersion() {
84

85
    UpgradeMode upgradeMode = this.mode;
×
86
    if (upgradeMode == null) {
×
87
      if (IdeVersion.getVersionString().contains("SNAPSHOT")) {
×
88
        upgradeMode = UpgradeMode.SNAPSHOT;
×
89
      } else {
90
        if (IdeVersion.getVersionIdentifier().getDevelopmentPhase().isStable()) {
×
91
          upgradeMode = UpgradeMode.STABLE;
×
92
        } else {
93
          upgradeMode = UpgradeMode.UNSTABLE;
×
94
        }
95
      }
96
    }
97
    return upgradeMode.getVersion();
×
98
  }
99

100
  @Override
101
  public Path getToolPath() {
102

103
    return this.context.getIdeInstallationPath();
×
104
  }
105

106
  @Override
107
  public boolean install(boolean silent) {
108

109
    if (!this.context.isOnline()) {
4✔
110
      this.context.warning("You are offline. IDEasy requires an internet connection to upgrade - skipping upgrade.");
4✔
111
      return false;
2✔
112
    }
113

114
    if (IdeVersion.isUndefined()) {
2!
115
      this.context.warning("You are using IDEasy version {} which indicates local development - skipping upgrade.", IdeVersion.getVersionString());
10✔
116
      return false;
2✔
117
    }
118
    return super.install(silent);
×
119
  }
120

121
  /**
122
   * @return the latest released {@link VersionIdentifier version} of IDEasy.
123
   */
124
  public VersionIdentifier getLatestVersion() {
125

126
    VersionIdentifier currentVersion = IdeVersion.getVersionIdentifier();
2✔
127
    if (IdeVersion.isUndefined()) {
2!
128
      return currentVersion;
2✔
129
    }
130
    VersionIdentifier configuredVersion = getConfiguredVersion();
×
131
    return getToolRepository().resolveVersion(this.tool, getConfiguredEdition(), configuredVersion, this);
×
132
  }
133

134
  /**
135
   * Checks if an update is available and logs according information.
136
   *
137
   * @return {@code true} if an update is available, {@code false} otherwise.
138
   */
139
  public boolean checkIfUpdateIsAvailable() {
140
    VersionIdentifier installedVersion = getInstalledVersion();
3✔
141
    if (!this.context.isOnline()) {
4✔
142
      this.context.success("Your version of IDEasy is {}.", installedVersion);
10✔
143
      this.context.warning("Skipping check for newer version of IDEasy due to lack of network connectivity.");
4✔
144
      return false;
2✔
145
    }
146
    VersionIdentifier latestVersion = getLatestVersion();
3✔
147
    if (installedVersion.equals(latestVersion)) {
4!
148
      this.context.success("Your version of IDEasy is {} which is the latest released version.", installedVersion);
10✔
149
      return false;
2✔
150
    } else {
151
      this.context.interaction("Your version of IDEasy is {} but version {} is available. Please run the following command to upgrade to the latest version:\n"
×
152
          + "ide upgrade", installedVersion, latestVersion);
153
      return true;
×
154
    }
155
  }
156

157
  /**
158
   * Initial installation of IDEasy.
159
   *
160
   * @param cwd the {@link Path} to the current working directory.
161
   * @see com.devonfw.tools.ide.commandlet.InstallCommandlet
162
   */
163
  public void installIdeasy(Path cwd) {
164
    Path ideRoot = determineIdeRoot(cwd);
4✔
165
    Path idePath = ideRoot.resolve(IdeContext.FOLDER_UNDERSCORE_IDE);
4✔
166
    Path installationPath = idePath.resolve(IdeContext.FOLDER_INSTALLATION);
4✔
167
    Path ideasySoftwarePath = idePath.resolve(IdeContext.FOLDER_SOFTWARE).resolve(MavenRepository.ID).resolve(IdeasyCommandlet.TOOL_NAME)
8✔
168
        .resolve(IdeasyCommandlet.TOOL_NAME);
2✔
169
    Path ideasyVersionPath = ideasySoftwarePath.resolve(IdeVersion.getVersionString());
4✔
170
    if (Files.isDirectory(ideasyVersionPath)) {
5!
171
      throw new CliException("IDEasy is already installed at " + ideasyVersionPath + " - if your installation is broken, delete it manually and rerun setup!");
×
172
    }
173
    FileAccess fileAccess = this.context.getFileAccess();
4✔
174
    List<Path> installationArtifacts = new ArrayList<>();
4✔
175
    boolean success = true;
2✔
176
    success &= addInstallationArtifact(cwd, "bin", true, installationArtifacts);
9✔
177
    success &= addInstallationArtifact(cwd, "functions", true, installationArtifacts);
9✔
178
    success &= addInstallationArtifact(cwd, "internal", true, installationArtifacts);
9✔
179
    success &= addInstallationArtifact(cwd, "system", true, installationArtifacts);
9✔
180
    success &= addInstallationArtifact(cwd, "IDEasy.pdf", true, installationArtifacts);
9✔
181
    success &= addInstallationArtifact(cwd, "setup", true, installationArtifacts);
9✔
182
    success &= addInstallationArtifact(cwd, "setup.bat", false, installationArtifacts);
9✔
183
    if (!success) {
2!
184
      throw new CliException("IDEasy release is inconsistent at " + cwd);
×
185
    }
186
    fileAccess.mkdirs(ideasyVersionPath);
3✔
187
    for (Path installationArtifact : installationArtifacts) {
10✔
188
      fileAccess.copy(installationArtifact, ideasyVersionPath);
4✔
189
    }
1✔
190
    this.context.writeVersionFile(IdeVersion.getVersionIdentifier(), ideasyVersionPath);
5✔
191
    fileAccess.symlink(ideasyVersionPath, installationPath);
4✔
192
    addToShellRc(BASHRC, ideRoot, null);
5✔
193
    addToShellRc(ZSHRC, ideRoot, "autoload -U +X bashcompinit && bashcompinit");
5✔
194
    installIdeasyWindowsEnv(ideRoot, installationPath);
4✔
195
    this.context.success("IDEasy has been installed successfully on your system.");
4✔
196
    this.context.warning("IDEasy has been setup for new shells but it cannot work in your current shell(s).\n"
4✔
197
        + "Reboot or open a new terminal to make it work.");
198
  }
1✔
199

200
  private void installIdeasyWindowsEnv(Path ideRoot, Path installationPath) {
201
    if (!this.context.getSystemInfo().isWindows()) {
5✔
202
      return;
1✔
203
    }
204
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
205
    helper.setUserEnvironmentValue(IdeVariables.IDE_ROOT.getName(), ideRoot.toString());
6✔
206
    String userPath = helper.getUserEnvironmentValue(IdeVariables.PATH.getName());
5✔
207
    if (userPath == null) {
2!
208
      this.context.error("Could not read user PATH from registry!");
×
209
    } else {
210
      this.context.info("Found user PATH={}", userPath);
10✔
211
      Path ideasyBinPath = installationPath.resolve("bin");
4✔
212
      SimpleSystemPath path = SimpleSystemPath.of(userPath, ';');
4✔
213
      if (path.getEntries().isEmpty()) {
4!
214
        this.context.warning("ATTENTION:\n"
×
215
            + "Your user specific PATH variable seems to be empty.\n"
216
            + "You can double check this by pressing [Windows][r] and launch the program SystemPropertiesAdvanced.\n"
217
            + "Then click on 'Environment variables' and check if 'PATH' is set in the 'user variables' from the upper list.\n"
218
            + "In case 'PATH' is defined there non-empty and you get this message, please abort and give us feedback:\n"
219
            + "https://github.com/devonfw/IDEasy/issues\n"
220
            + "Otherwise all is correct and you can continue.");
221
        this.context.askToContinue("Are you sure you want to override your PATH?");
×
222
      } else {
223
        path.removeEntries(s -> s.endsWith(IDE_BIN));
7✔
224
      }
225
      path.getEntries().add(ideasyBinPath.toString());
6✔
226
      helper.setUserEnvironmentValue(IdeVariables.PATH.getName(), path.toString());
6✔
227
      setGitLongpaths();
2✔
228
    }
229
  }
1✔
230

231
  private void setGitLongpaths() {
232
    GitContext gitContext = new GitContextImpl(this.context);
6✔
233
    gitContext.verifyGitInstalled();
2✔
234
    setGitConfigProperty("core", "longpaths", "true", this.context.getUserHome().resolve(".gitconfig"));
10✔
235
  }
1✔
236

237
  /**
238
   * @param section the section to modify
239
   * @param property the property to set
240
   * @param value the value to set
241
   * @param configPath the path of the config file
242
   */
243
  public void setGitConfigProperty(String section, String property, String value, Path configPath) {
244
    String gitconfig;
245
    // read files
246
    FileAccess fileAccess = this.context.getFileAccess();
4✔
247
    gitconfig = fileAccess.readFileContent(configPath);
4✔
248
    if (gitconfig == null) {
2✔
249
      gitconfig = "";
2✔
250
    }
251

252
    LinkedHashMap<String, LinkedHashMap<String, String>> iniMap = parseIniFile(gitconfig);
4✔
253

254
    // check if section exists
255
    if (!iniMap.containsKey(section)) {
4✔
256
      iniMap.put(section, new LinkedHashMap<>());
7✔
257
    }
258

259
    // set property
260
    iniMap.get(section).put(property, value);
8✔
261

262
    // write out new file
263
    StringBuilder newConfig = new StringBuilder();
4✔
264
    for (String configSection : iniMap.keySet()) {
11✔
265
      newConfig.append(String.format("[%s]\n", configSection));
11✔
266
      LinkedHashMap<String, String> properties = iniMap.get(configSection);
5✔
267
      for (String sectionProperty : properties.keySet()) {
11✔
268
        String propertyValue = properties.get(sectionProperty);
5✔
269
        newConfig.append(String.format("\t%s = %s\n", sectionProperty, propertyValue));
15✔
270
      }
1✔
271
    }
1✔
272

273
    try {
274
      Files.writeString(configPath, newConfig.toString());
7✔
275
    } catch (IOException e) {
×
276
      this.context.error("could not write git config file at %s", configPath);
×
277
    }
1✔
278
  }
1✔
279

280
  private LinkedHashMap<String, LinkedHashMap<String, String>> parseIniFile(String iniFile) {
281
    List<String> iniLines = iniFile.lines().toList();
4✔
282
    LinkedHashMap<String, LinkedHashMap<String, String>> iniMap = new LinkedHashMap<>();
4✔
283
    String currentSection = "";
2✔
284
    for (String line : iniLines) {
10✔
285
      if (line.isEmpty()) {
3!
286
        continue;
×
287
      }
288
      if (line.startsWith("[")) {
4✔
289
        currentSection = line.replace("[", "").replace("]", "");
8✔
290
        iniMap.put(currentSection, new LinkedHashMap<>());
8✔
291
      } else {
292
        String[] parts = line.split("=");
4✔
293
        String propertyName = parts[0].trim();
5✔
294
        String propertyValue = parts[1].trim();
5✔
295
        iniMap.get(currentSection).put(propertyName, propertyValue);
8✔
296
      }
297
    }
1✔
298
    return iniMap;
2✔
299
  }
300

301
  static String removeObsoleteEntryFromWindowsPath(String userPath) {
302
    return removeEntryFromWindowsPath(userPath, IDE_BIN);
4✔
303
  }
304

305
  static String removeEntryFromWindowsPath(String userPath, String suffix) {
306
    int len = userPath.length();
3✔
307
    int start = 0;
2✔
308
    while ((start >= 0) && (start < len)) {
5!
309
      int end = userPath.indexOf(';', start);
5✔
310
      if (end < 0) {
2✔
311
        end = len;
2✔
312
      }
313
      String entry = userPath.substring(start, end);
5✔
314
      if (entry.endsWith(suffix)) {
4✔
315
        String prefix = "";
2✔
316
        int offset = 1;
2✔
317
        if (start > 0) {
2✔
318
          prefix = userPath.substring(0, start - 1);
7✔
319
          offset = 0;
2✔
320
        }
321
        if (end == len) {
3✔
322
          return prefix;
2✔
323
        } else {
324
          return removeEntryFromWindowsPath(prefix + userPath.substring(end + offset), suffix);
10✔
325
        }
326
      }
327
      start = end + 1;
4✔
328
    }
1✔
329
    return userPath;
2✔
330
  }
331

332
  /**
333
   * Adds ourselves to the shell RC (run-commands) configuration file.
334
   *
335
   * @param filename the name of the RC file.
336
   * @param ideRoot the IDE_ROOT {@link Path}.
337
   */
338
  private void addToShellRc(String filename, Path ideRoot, String extraLine) {
339

340
    modifyShellRc(filename, ideRoot, true, extraLine);
6✔
341
  }
1✔
342

343
  private void removeFromShellRc(String filename, Path ideRoot) {
344

345
    modifyShellRc(filename, ideRoot, false, null);
6✔
346
  }
1✔
347

348
  /**
349
   * Adds ourselves to the shell RC (run-commands) configuration file.
350
   *
351
   * @param filename the name of the RC file.
352
   * @param ideRoot the IDE_ROOT {@link Path}.
353
   */
354
  private void modifyShellRc(String filename, Path ideRoot, boolean add, String extraLine) {
355

356
    if (add) {
2✔
357
      this.context.info("Configuring IDEasy in {}", filename);
11✔
358
    } else {
359
      this.context.info("Removing IDEasy from {}", filename);
10✔
360
    }
361
    Path rcFile = this.context.getUserHome().resolve(filename);
6✔
362
    FileAccess fileAccess = this.context.getFileAccess();
4✔
363
    List<String> lines = fileAccess.readFileLines(rcFile);
4✔
364
    if (lines == null) {
2✔
365
      if (!add) {
2!
366
        return;
×
367
      }
368
      lines = new ArrayList<>();
5✔
369
    } else {
370
      // since it is unspecified if the returned List may be immutable we want to get sure
371
      lines = new ArrayList<>(lines);
5✔
372
    }
373
    Iterator<String> iterator = lines.iterator();
3✔
374
    int removeCount = 0;
2✔
375
    while (iterator.hasNext()) {
3✔
376
      String line = iterator.next();
4✔
377
      line = line.trim();
3✔
378
      if (isObsoleteRcLine(line)) {
3✔
379
        this.context.info("Removing obsolete line from {}: {}", filename, line);
14✔
380
        iterator.remove();
2✔
381
        removeCount++;
2✔
382
      } else if (line.equals(extraLine)) {
4✔
383
        extraLine = null;
2✔
384
      }
385
    }
1✔
386
    if (add) {
2✔
387
      if (extraLine != null) {
2!
388
        lines.add(extraLine);
×
389
      }
390
      if (!this.context.getSystemInfo().isWindows()) {
5✔
391
        lines.add("export IDE_ROOT=\"" + WindowsPathSyntax.MSYS.format(ideRoot) + "\"");
7✔
392
      }
393
      lines.add(BASH_CODE_SOURCE_FUNCTIONS);
4✔
394
    }
395
    fileAccess.writeFileLines(lines, rcFile);
4✔
396
    this.context.debug("Successfully updated {}", filename);
10✔
397
  }
1✔
398

399
  private static boolean isObsoleteRcLine(String line) {
400
    if (line.startsWith("alias ide=")) {
4!
401
      return true;
×
402
    } else if (line.startsWith("export IDE_ROOT=")) {
4!
403
      return true;
×
404
    } else if (line.equals("ide")) {
4✔
405
      return true;
2✔
406
    } else if (line.equals("ide init")) {
4✔
407
      return true;
2✔
408
    } else if (line.startsWith("source \"$IDE_ROOT/_ide/")) {
4✔
409
      return true;
2✔
410
    }
411
    return false;
2✔
412
  }
413

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

416
    Path artifactPath = cwd.resolve(artifactName);
4✔
417
    if (Files.exists(artifactPath)) {
5!
418
      installationArtifacts.add(artifactPath);
5✔
419
    } else if (required) {
×
420
      this.context.error("Missing required file {}", artifactName);
×
421
      return false;
×
422
    }
423
    return true;
2✔
424
  }
425

426
  private Path determineIdeRoot(Path cwd) {
427
    Path ideRoot = this.context.getIdeRoot();
4✔
428
    if (ideRoot == null) {
2!
429
      Path home = this.context.getUserHome();
4✔
430
      Path installRoot = home;
2✔
431
      if (this.context.getSystemInfo().isWindows()) {
5✔
432
        if (!cwd.startsWith(home)) {
4!
433
          installRoot = cwd.getRoot();
×
434
        }
435
      }
436
      ideRoot = installRoot.resolve(IdeContext.FOLDER_PROJECTS);
4✔
437
    } else {
1✔
438
      assert (Files.isDirectory(ideRoot)) : "IDE_ROOT directory does not exist!";
×
439
    }
440
    return ideRoot;
2✔
441
  }
442

443
  /**
444
   * Uninstalls IDEasy entirely from the system.
445
   */
446
  public void uninstallIdeasy() {
447

448
    Path ideRoot = this.context.getIdeRoot();
4✔
449
    removeFromShellRc(BASHRC, ideRoot);
4✔
450
    removeFromShellRc(ZSHRC, ideRoot);
4✔
451
    Path idePath = this.context.getIdePath();
4✔
452
    uninstallIdeasyWindowsEnv(ideRoot);
3✔
453
    uninstallIdeasyIdePath(idePath);
3✔
454
    deleteDownloadCache();
2✔
455
    this.context.success("IDEasy has been uninstalled from your system.");
4✔
456
    this.context.interaction("ATTENTION:\n"
10✔
457
        + "In order to prevent data-loss, we do not delete your projects and git repositories!\n"
458
        + "To entirely get rid of IDEasy, also check your IDE_ROOT folder at:\n"
459
        + "{}", ideRoot);
460
  }
1✔
461

462
  private void deleteDownloadCache() {
463
    Path downloadPath = this.context.getDownloadPath();
4✔
464
    this.context.info("Deleting download cache from {}", downloadPath);
10✔
465
    this.context.getFileAccess().delete(downloadPath);
5✔
466
  }
1✔
467

468
  private void uninstallIdeasyIdePath(Path idePath) {
469
    if (this.context.getSystemInfo().isWindows()) {
5✔
470
      this.context.newProcess().executable("bash").addArgs("-c",
17✔
471
          "sleep 10 && rm -rf \"" + WindowsPathSyntax.MSYS.format(idePath) + "\"").run(ProcessMode.BACKGROUND);
5✔
472
      this.context.interaction("To prevent windows file locking errors, we perform an asynchronous deletion of {} in background now.\n"
11✔
473
          + "Please close all terminals and wait a minute for the deletion to complete before running other commands.", idePath);
474
    } else {
475
      this.context.info("Finally deleting {}", idePath);
10✔
476
      this.context.getFileAccess().delete(idePath);
5✔
477
    }
478
  }
1✔
479

480
  private void uninstallIdeasyWindowsEnv(Path ideRoot) {
481
    if (!this.context.getSystemInfo().isWindows()) {
5✔
482
      return;
1✔
483
    }
484
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
485
    helper.removeUserEnvironmentValue(IdeVariables.IDE_ROOT.getName());
4✔
486
    String userPath = helper.getUserEnvironmentValue(IdeVariables.PATH.getName());
5✔
487
    if (userPath == null) {
2!
488
      this.context.error("Could not read user PATH from registry!");
×
489
    } else {
490
      this.context.info("Found user PATH={}", userPath);
10✔
491
      String newUserPath = userPath;
2✔
492
      if (!userPath.isEmpty()) {
3!
493
        SimpleSystemPath path = SimpleSystemPath.of(userPath, ';');
4✔
494
        path.removeEntries(s -> s.endsWith(IDE_BIN) || s.endsWith(IDE_INSTALLATION_BIN));
15!
495
        newUserPath = path.toString();
3✔
496
      }
497
      if (newUserPath.equals(userPath)) {
4!
498
        this.context.error("Could not find IDEasy in PATH:\n{}", userPath);
×
499
      } else {
500
        helper.setUserEnvironmentValue(IdeVariables.PATH.getName(), newUserPath);
5✔
501
      }
502
    }
503
  }
1✔
504
}
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