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

devonfw / IDEasy / 15213103217

23 May 2025 02:49PM UTC coverage: 67.807% (+0.09%) from 67.716%
15213103217

push

github

web-flow
#1293: make sure core.longpaths is true in windows git config (#1315)

Co-authored-by: Jörg Hohwiller <hohwille@users.noreply.github.com>

3138 of 5036 branches covered (62.31%)

Branch coverage included in aggregate %.

8061 of 11480 relevant lines covered (70.22%)

3.07 hits per line

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

81.41
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.SystemPath;
16
import com.devonfw.tools.ide.common.Tag;
17
import com.devonfw.tools.ide.context.IdeContext;
18
import com.devonfw.tools.ide.io.FileAccess;
19
import com.devonfw.tools.ide.os.WindowsHelper;
20
import com.devonfw.tools.ide.os.WindowsPathSyntax;
21
import com.devonfw.tools.ide.process.ProcessMode;
22
import com.devonfw.tools.ide.tool.mvn.MvnArtifact;
23
import com.devonfw.tools.ide.tool.mvn.MvnBasedLocalToolCommandlet;
24
import com.devonfw.tools.ide.tool.repository.MavenRepository;
25
import com.devonfw.tools.ide.variable.IdeVariables;
26
import com.devonfw.tools.ide.version.IdeVersion;
27
import com.devonfw.tools.ide.version.VersionIdentifier;
28

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

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

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

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

46
  private final UpgradeMode mode;
47

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

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

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

69
  @Override
70
  public VersionIdentifier getInstalledVersion() {
71

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

75
  @Override
76
  public String getConfiguredEdition() {
77

78
    return this.tool;
×
79
  }
80

81
  @Override
82
  public VersionIdentifier getConfiguredVersion() {
83

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

99
  @Override
100
  public Path getToolPath() {
101

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

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

108
    if (IdeVersion.isUndefined()) {
2!
109
      this.context.warning("You are using IDEasy version {} which indicates local development - skipping upgrade.", IdeVersion.getVersionString());
10✔
110
      return false;
2✔
111
    }
112
    return super.install(silent);
×
113
  }
114

115
  /**
116
   * @return the latest released {@link VersionIdentifier version} of IDEasy.
117
   */
118
  public VersionIdentifier getLatestVersion() {
119

120
    VersionIdentifier currentVersion = IdeVersion.getVersionIdentifier();
2✔
121
    if (IdeVersion.isUndefined()) {
2!
122
      return currentVersion;
2✔
123
    }
124
    VersionIdentifier configuredVersion = getConfiguredVersion();
×
125
    return getToolRepository().resolveVersion(this.tool, getConfiguredEdition(), configuredVersion, this);
×
126
  }
127

128
  /**
129
   * Checks if an update is available and logs according information.
130
   *
131
   * @return {@code true} if an update is available, {@code false} otherwise.
132
   */
133
  public boolean checkIfUpdateIsAvailable() {
134
    VersionIdentifier installedVersion = getInstalledVersion();
3✔
135
    VersionIdentifier latestVersion = getLatestVersion();
3✔
136
    if (installedVersion.equals(latestVersion)) {
4!
137
      this.context.success("Your version of IDEasy is {} which is the latest released version.", installedVersion);
10✔
138
      return false;
2✔
139
    } else {
140
      this.context.interaction("Your version of IDEasy is {} but version {} is available. Please run the following command to upgrade to the latest version:\n"
×
141
          + "ide upgrade", installedVersion, latestVersion);
142
      return true;
×
143
    }
144
  }
145

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

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

220
  private void setGitLongpaths() {
221
    SystemPath systemPath = this.context.getPath();
4✔
222
    Path gitPath = systemPath.getPath("git");
4✔
223
    if (gitPath == null) {
2!
224
      this.context.error("Git is not installed on your computer but required by IDEasy. Please download and install git:\n"
5✔
225
          + "https://git-scm.com/download/");
226
    } else {
227
      setGitConfigProperty("core", "longpaths", "true", this.context.getUserHome().resolve(".gitconfig"));
×
228
    }
229
  }
1✔
230

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

246
    LinkedHashMap<String, LinkedHashMap<String, String>> iniMap = parseIniFile(gitconfig);
4✔
247

248
    // check if section exists
249
    if (!iniMap.containsKey(section)) {
4✔
250
      iniMap.put(section, new LinkedHashMap<>());
7✔
251
    }
252

253
    // set property
254
    iniMap.get(section).put(property, value);
8✔
255

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

267
    try {
268
      Files.writeString(configPath, newConfig.toString());
7✔
269
    } catch (IOException e) {
×
270
      this.context.error("could not write git config file at %s", configPath);
×
271
    }
1✔
272
  }
1✔
273

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

295
  static String removeObsoleteEntryFromWindowsPath(String userPath) {
296
    return removeEntryFromWindowsPath(userPath, IDE_BIN);
4✔
297
  }
298

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

326
  /**
327
   * Adds ourselves to the shell RC (run-commands) configuration file.
328
   *
329
   * @param filename the name of the RC file.
330
   * @param ideRoot the IDE_ROOT {@link Path}.
331
   */
332
  private void addToShellRc(String filename, Path ideRoot, String extraLine) {
333

334
    modifyShellRc(filename, ideRoot, true, extraLine);
6✔
335
  }
1✔
336

337
  private void removeFromShellRc(String filename, Path ideRoot) {
338

339
    modifyShellRc(filename, ideRoot, false, null);
6✔
340
  }
1✔
341

342
  /**
343
   * Adds ourselves to the shell RC (run-commands) configuration file.
344
   *
345
   * @param filename the name of the RC file.
346
   * @param ideRoot the IDE_ROOT {@link Path}.
347
   */
348
  private void modifyShellRc(String filename, Path ideRoot, boolean add, String extraLine) {
349

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

393
  private static boolean isObsoleteRcLine(String line) {
394
    if (line.startsWith("alias ide=")) {
4!
395
      return true;
×
396
    } else if (line.startsWith("export IDE_ROOT=")) {
4!
397
      return true;
×
398
    } else if (line.equals("ide")) {
4✔
399
      return true;
2✔
400
    } else if (line.equals("ide init")) {
4✔
401
      return true;
2✔
402
    } else if (line.startsWith("source \"$IDE_ROOT/_ide/")) {
4✔
403
      return true;
2✔
404
    }
405
    return false;
2✔
406
  }
407

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

410
    Path artifactPath = cwd.resolve(artifactName);
4✔
411
    if (Files.exists(artifactPath)) {
5!
412
      installationArtifacts.add(artifactPath);
5✔
413
    } else if (required) {
×
414
      this.context.error("Missing required file {}", artifactName);
×
415
      return false;
×
416
    }
417
    return true;
2✔
418
  }
419

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

437
  /**
438
   * Uninstalls IDEasy entirely from the system.
439
   */
440
  public void uninstallIdeasy() {
441

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

456
  private void deleteDownloadCache() {
457
    Path downloadPath = this.context.getDownloadPath();
4✔
458
    this.context.info("Deleting download cache from {}", downloadPath);
10✔
459
    this.context.getFileAccess().delete(downloadPath);
5✔
460
  }
1✔
461

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

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