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

devonfw / IDEasy / 24087875064

07 Apr 2026 02:51PM UTC coverage: 70.496% (+0.03%) from 70.47%
24087875064

Pull #1803

github

web-flow
Merge 8455c4c76 into c0f5fa9cf
Pull Request #1803: #1760: accept empty input for one option

4270 of 6696 branches covered (63.77%)

Branch coverage included in aggregate %.

11077 of 15074 relevant lines covered (73.48%)

3.1 hits per line

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

66.19
cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java
1
package com.devonfw.tools.ide.context;
2

3
import static com.devonfw.tools.ide.variable.IdeVariables.IDE_MIN_VERSION;
4

5
import java.io.ByteArrayInputStream;
6
import java.io.ByteArrayOutputStream;
7
import java.io.IOException;
8
import java.nio.file.Files;
9
import java.nio.file.Path;
10
import java.time.LocalDateTime;
11
import java.util.ArrayList;
12
import java.util.HashMap;
13
import java.util.Iterator;
14
import java.util.List;
15
import java.util.Locale;
16
import java.util.Map;
17
import java.util.Map.Entry;
18
import java.util.Objects;
19
import java.util.Properties;
20
import java.util.function.Predicate;
21
import java.util.logging.FileHandler;
22
import java.util.logging.LogManager;
23
import java.util.logging.SimpleFormatter;
24

25
import org.slf4j.Logger;
26
import org.slf4j.LoggerFactory;
27

28
import com.devonfw.tools.ide.cli.CliAbortException;
29
import com.devonfw.tools.ide.cli.CliArgument;
30
import com.devonfw.tools.ide.cli.CliArguments;
31
import com.devonfw.tools.ide.cli.CliException;
32
import com.devonfw.tools.ide.commandlet.Commandlet;
33
import com.devonfw.tools.ide.commandlet.CommandletManager;
34
import com.devonfw.tools.ide.commandlet.CommandletManagerImpl;
35
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
36
import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet;
37
import com.devonfw.tools.ide.common.SystemPath;
38
import com.devonfw.tools.ide.completion.CompletionCandidate;
39
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
40
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault;
41
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
42
import com.devonfw.tools.ide.environment.EnvironmentVariables;
43
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
44
import com.devonfw.tools.ide.environment.IdeSystem;
45
import com.devonfw.tools.ide.environment.IdeSystemImpl;
46
import com.devonfw.tools.ide.git.GitContext;
47
import com.devonfw.tools.ide.git.GitContextImpl;
48
import com.devonfw.tools.ide.git.GitUrl;
49
import com.devonfw.tools.ide.io.FileAccess;
50
import com.devonfw.tools.ide.io.FileAccessImpl;
51
import com.devonfw.tools.ide.log.IdeLogArgFormatter;
52
import com.devonfw.tools.ide.log.IdeLogLevel;
53
import com.devonfw.tools.ide.log.IdeLogListener;
54
import com.devonfw.tools.ide.log.JulConsoleHandler;
55
import com.devonfw.tools.ide.merge.DirectoryMerger;
56
import com.devonfw.tools.ide.migration.IdeMigrator;
57
import com.devonfw.tools.ide.network.NetworkStatus;
58
import com.devonfw.tools.ide.network.NetworkStatusImpl;
59
import com.devonfw.tools.ide.os.SystemInfo;
60
import com.devonfw.tools.ide.os.SystemInfoImpl;
61
import com.devonfw.tools.ide.os.WindowsHelper;
62
import com.devonfw.tools.ide.os.WindowsHelperImpl;
63
import com.devonfw.tools.ide.os.WindowsPathSyntax;
64
import com.devonfw.tools.ide.process.ProcessContext;
65
import com.devonfw.tools.ide.process.ProcessContextImpl;
66
import com.devonfw.tools.ide.process.ProcessResult;
67
import com.devonfw.tools.ide.property.Property;
68
import com.devonfw.tools.ide.step.Step;
69
import com.devonfw.tools.ide.step.StepImpl;
70
import com.devonfw.tools.ide.tool.custom.CustomToolRepository;
71
import com.devonfw.tools.ide.tool.custom.CustomToolRepositoryImpl;
72
import com.devonfw.tools.ide.tool.mvn.MvnRepository;
73
import com.devonfw.tools.ide.tool.npm.NpmRepository;
74
import com.devonfw.tools.ide.tool.pip.PipRepository;
75
import com.devonfw.tools.ide.tool.repository.DefaultToolRepository;
76
import com.devonfw.tools.ide.tool.repository.ToolRepository;
77
import com.devonfw.tools.ide.url.model.UrlMetadata;
78
import com.devonfw.tools.ide.util.DateTimeUtil;
79
import com.devonfw.tools.ide.util.PrivacyUtil;
80
import com.devonfw.tools.ide.validation.ValidationResult;
81
import com.devonfw.tools.ide.validation.ValidationResultValid;
82
import com.devonfw.tools.ide.validation.ValidationState;
83
import com.devonfw.tools.ide.variable.IdeVariables;
84
import com.devonfw.tools.ide.version.IdeVersion;
85
import com.devonfw.tools.ide.version.VersionIdentifier;
86

87
/**
88
 * Abstract base implementation of {@link IdeContext}.
89
 */
90
public abstract class AbstractIdeContext implements IdeContext, IdeLogArgFormatter {
91

92
  static final Logger LOG = LoggerFactory.getLogger(AbstractIdeContext.class);
3✔
93

94
  /** The default shell bash (Bourne Again SHell). */
95
  public static final String BASH = "bash";
96

97
  private static final GitUrl IDE_URLS_GIT = new GitUrl("https://github.com/devonfw/ide-urls.git", null);
7✔
98

99
  private static final String LICENSE_URL = "https://github.com/devonfw/IDEasy/blob/main/documentation/LICENSE.adoc";
100

101
  private static final String DEFAULT_WINDOWS_GIT_PATH = "C:\\Program Files\\Git\\bin\\bash.exe";
102

103
  private static final String OPTION_DETAILS_START = "([";
104

105
  private final IdeStartContextImpl startContext;
106

107
  private Path ideHome;
108

109
  private final Path ideRoot;
110

111
  private Path confPath;
112

113
  protected Path settingsPath;
114

115
  private Path settingsCommitIdPath;
116

117
  protected Path pluginsPath;
118

119
  private Path workspacePath;
120

121
  private Path workspacesBasePath;
122

123
  private String workspaceName;
124

125
  private Path cwd;
126

127
  private Path downloadPath;
128

129
  private Path userHome;
130

131
  private Path userHomeIde;
132

133
  private SystemPath path;
134

135
  private WindowsPathSyntax pathSyntax;
136

137
  private final SystemInfo systemInfo;
138

139
  private EnvironmentVariables variables;
140

141
  private final FileAccess fileAccess;
142

143
  protected CommandletManager commandletManager;
144

145
  protected ToolRepository defaultToolRepository;
146

147
  private CustomToolRepository customToolRepository;
148

149
  private MvnRepository mvnRepository;
150

151
  private NpmRepository npmRepository;
152

153
  private PipRepository pipRepository;
154

155
  private DirectoryMerger workspaceMerger;
156

157
  protected UrlMetadata urlMetadata;
158

159
  protected Path defaultExecutionDirectory;
160

161
  private StepImpl currentStep;
162

163
  private NetworkStatus networkStatus;
164

165
  protected IdeSystem system;
166

167
  private WindowsHelper windowsHelper;
168

169
  private final Map<String, String> privacyMap;
170

171
  private Path bash;
172

173
  private boolean julConfigured;
174

175
  private Path logfile;
176

177
  /**
178
   * The constructor.
179
   *
180
   * @param startContext the {@link IdeStartContextImpl}.
181
   * @param workingDirectory the optional {@link Path} to current working directory.
182
   */
183
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
184

185
    super();
2✔
186
    this.startContext = startContext;
3✔
187
    this.startContext.setArgFormatter(this);
4✔
188
    this.privacyMap = new HashMap<>();
5✔
189
    this.systemInfo = SystemInfoImpl.INSTANCE;
3✔
190
    if (isTest()) {
3!
191
      configureJavaUtilLogging(null);
3✔
192
    }
193
    this.commandletManager = new CommandletManagerImpl(this);
6✔
194
    this.fileAccess = new FileAccessImpl(this);
6✔
195
    String userHomeProperty = getSystem().getProperty("user.home");
5✔
196
    if (userHomeProperty != null) {
2!
197
      this.userHome = Path.of(userHomeProperty);
×
198
    }
199
    if (workingDirectory == null) {
2!
200
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
201
    }
202
    workingDirectory = workingDirectory.toAbsolutePath();
3✔
203
    if (Files.isDirectory(workingDirectory)) {
5✔
204
      workingDirectory = this.fileAccess.toCanonicalPath(workingDirectory);
6✔
205
    } else {
206
      LOG.warn("Current working directory does not exist: {}", workingDirectory);
4✔
207
    }
208
    this.cwd = workingDirectory;
3✔
209
    // detect IDE_HOME and WORKSPACE
210
    String workspace = null;
2✔
211
    Path ideHomeDir = null;
2✔
212
    IdeHomeAndWorkspace ideHomeAndWorkspace = findIdeHome(workingDirectory);
4✔
213
    if (ideHomeAndWorkspace != null) {
2!
214
      ideHomeDir = ideHomeAndWorkspace.home();
3✔
215
      workspace = ideHomeAndWorkspace.workspace();
3✔
216
    }
217

218
    // detection completed, initializing variables
219
    this.ideRoot = findIdeRoot(ideHomeDir);
5✔
220

221
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
222

223
    if (this.ideRoot != null) {
3✔
224
      Path tempDownloadPath = getTempDownloadPath();
3✔
225
      if (Files.isDirectory(tempDownloadPath)) {
6✔
226
        // TODO delete all files older than 1 day here...
227
      } else {
228
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
229
      }
230
    }
231
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
232
  }
1✔
233

234
  /**
235
   * Searches for the IDE home directory by traversing up the directory tree from the given working directory. This method can be overridden in test contexts to
236
   * add additional validation or boundary checks.
237
   *
238
   * @param workingDirectory the starting directory for the search.
239
   * @return an instance of {@link IdeHomeAndWorkspace} where the IDE_HOME was found or {@code null} if not found.
240
   */
241
  protected IdeHomeAndWorkspace findIdeHome(Path workingDirectory) {
242

243
    Path currentDir = workingDirectory;
2✔
244
    String name1 = "";
2✔
245
    String name2 = "";
2✔
246
    String workspace = WORKSPACE_MAIN;
2✔
247
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
248

249
    while (currentDir != null) {
2✔
250
      LOG.trace("Looking for IDE_HOME in {}", currentDir);
4✔
251
      if (isIdeHome(currentDir)) {
4✔
252
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
253
          workspace = name2;
3✔
254
        }
255
        break;
256
      }
257
      name2 = name1;
2✔
258
      int nameCount = currentDir.getNameCount();
3✔
259
      if (nameCount >= 1) {
3✔
260
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
261
      }
262
      currentDir = currentDir.getParent();
3✔
263
      if ((ideRootPath != null) && (ideRootPath.equals(currentDir))) {
2!
264
        // prevent that during tests we traverse to the real IDE project of IDEasy developer
265
        currentDir = null;
×
266
      }
267
    }
1✔
268

269
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
270
  }
271

272
  /**
273
   * @return a new {@link MvnRepository}
274
   */
275
  protected MvnRepository createMvnRepository() {
276
    return new MvnRepository(this);
5✔
277
  }
278

279
  /**
280
   * @return a new {@link NpmRepository}
281
   */
282
  protected NpmRepository createNpmRepository() {
283
    return new NpmRepository(this);
×
284
  }
285

286
  /**
287
   * @return a new {@link PipRepository}
288
   */
289
  protected PipRepository createPipRepository() {
290
    return new PipRepository(this);
×
291
  }
292

293
  private Path findIdeRoot(Path ideHomePath) {
294

295
    Path ideRootPath = null;
2✔
296
    if (ideHomePath != null) {
2✔
297
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
298
      ideRootPath = ideHomePath.getParent();
3✔
299
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
2!
300
        LOG.warn(
×
301
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
302
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
303
            ideRootPathFromEnv,
304
            ideHomePath.getFileName(), ideRootPath);
×
305
      }
306
    } else if (!isTest()) {
4!
307
      ideRootPath = getIdeRootPathFromEnv(true);
×
308
    }
309
    return ideRootPath;
2✔
310
  }
311

312
  /**
313
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
314
   */
315
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
316

317
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
318
    if (root != null) {
×
319
      Path rootPath = Path.of(root);
×
320
      if (Files.isDirectory(rootPath)) {
×
321
        Path absoluteRootPath = getFileAccess().toCanonicalPath(rootPath);
×
322
        if (withSanityCheck) {
×
323
          int nameCount = rootPath.getNameCount();
×
324
          int absoluteNameCount = absoluteRootPath.getNameCount();
×
325
          int delta = absoluteNameCount - nameCount;
×
326
          if (delta >= 0) {
×
327
            for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) {
×
328
              String rootName = rootPath.getName(nameIndex).toString();
×
329
              String absoluteRootName = absoluteRootPath.getName(nameIndex + delta).toString();
×
330
              if (!rootName.equals(absoluteRootName)) {
×
331
                LOG.warn("IDE_ROOT is set to {} but was expanded to absolute path {} and does not match for segment {} and {} - fix your IDEasy installation!",
×
332
                    rootPath, absoluteRootPath, rootName, absoluteRootName);
333
                break;
×
334
              }
335
            }
336
          } else {
337
            LOG.warn("IDE_ROOT is set to {} but was expanded to a shorter absolute path {}", rootPath,
×
338
                absoluteRootPath);
339
          }
340
        }
341
        return absoluteRootPath;
×
342
      } else if (withSanityCheck) {
×
343
        LOG.warn("IDE_ROOT is set to {} that is not an existing directory - fix your IDEasy installation!", rootPath);
×
344
      }
345
    }
346
    return null;
×
347
  }
348

349
  @Override
350
  public void setCwd(Path userDir, String workspace, Path ideHome) {
351

352
    this.cwd = userDir;
3✔
353
    this.workspaceName = workspace;
3✔
354
    this.ideHome = ideHome;
3✔
355
    if (ideHome == null) {
2✔
356
      this.workspacesBasePath = null;
3✔
357
      this.workspacePath = null;
3✔
358
      this.confPath = null;
3✔
359
      this.settingsPath = null;
3✔
360
      this.pluginsPath = null;
4✔
361
    } else {
362
      this.workspacesBasePath = this.ideHome.resolve(FOLDER_WORKSPACES);
6✔
363
      this.workspacePath = this.workspacesBasePath.resolve(this.workspaceName);
7✔
364
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
365
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
366
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
367
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
368
    }
369
    if (isTest()) {
3!
370
      // only for testing...
371
      if (this.ideHome == null) {
3✔
372
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
373
      } else {
374
        this.userHome = this.ideHome.resolve("home");
6✔
375
      }
376
    }
377
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
378
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
379
    resetPrivacyMap();
2✔
380
    this.path = computeSystemPath();
4✔
381
  }
1✔
382

383
  private String getMessageIdeHomeFound() {
384

385
    String wks = this.workspaceName;
3✔
386
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
387
      wks = "*".repeat(wks.length());
×
388
    }
389
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
390
  }
391

392
  private String getMessageNotInsideIdeProject() {
393

394
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
395
  }
396

397
  private String getMessageIdeRootNotFound() {
398

399
    String root = getSystem().getEnv("IDE_ROOT");
5✔
400
    if (root == null) {
2!
401
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
402
    } else {
403
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
404
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
405
    }
406
  }
407

408
  /**
409
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
410
   */
411
  public boolean isTest() {
412

413
    return false;
×
414
  }
415

416
  protected SystemPath computeSystemPath() {
417

418
    return new SystemPath(this);
×
419
  }
420

421
  /**
422
   * Checks if the given directory is a valid IDE home by verifying it contains both 'workspaces' and 'settings' directories.
423
   *
424
   * @param dir the directory to check.
425
   * @return {@code true} if the directory is a valid IDE home, {@code false} otherwise.
426
   */
427
  protected boolean isIdeHome(Path dir) {
428

429
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
430
      return false;
2✔
431
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
432
      return false;
×
433
    }
434
    return true;
2✔
435
  }
436

437
  private EnvironmentVariables createVariables() {
438

439
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
440
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
441
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
442
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
443
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
444
    return conf.resolved();
3✔
445
  }
446

447
  protected AbstractEnvironmentVariables createSystemVariables() {
448

449
    return EnvironmentVariables.ofSystem(this);
3✔
450
  }
451

452
  @Override
453
  public SystemInfo getSystemInfo() {
454

455
    return this.systemInfo;
3✔
456
  }
457

458
  @Override
459
  public FileAccess getFileAccess() {
460

461
    return this.fileAccess;
3✔
462
  }
463

464
  @Override
465
  public CommandletManager getCommandletManager() {
466

467
    return this.commandletManager;
3✔
468
  }
469

470
  @Override
471
  public ToolRepository getDefaultToolRepository() {
472

473
    return this.defaultToolRepository;
3✔
474
  }
475

476
  @Override
477
  public MvnRepository getMvnRepository() {
478
    if (this.mvnRepository == null) {
3✔
479
      this.mvnRepository = createMvnRepository();
4✔
480
    }
481
    return this.mvnRepository;
3✔
482
  }
483

484
  @Override
485
  public NpmRepository getNpmRepository() {
486
    if (this.npmRepository == null) {
3✔
487
      this.npmRepository = createNpmRepository();
4✔
488
    }
489
    return this.npmRepository;
3✔
490
  }
491

492
  @Override
493
  public PipRepository getPipRepository() {
494
    if (this.pipRepository == null) {
3✔
495
      this.pipRepository = createPipRepository();
4✔
496
    }
497
    return this.pipRepository;
3✔
498
  }
499

500
  @Override
501
  public CustomToolRepository getCustomToolRepository() {
502

503
    if (this.customToolRepository == null) {
3!
504
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
505
    }
506
    return this.customToolRepository;
3✔
507
  }
508

509
  @Override
510
  public Path getIdeHome() {
511

512
    return this.ideHome;
3✔
513
  }
514

515
  @Override
516
  public String getProjectName() {
517

518
    if (this.ideHome != null) {
3!
519
      return this.ideHome.getFileName().toString();
5✔
520
    }
521
    return "";
×
522
  }
523

524
  @Override
525
  public VersionIdentifier getProjectVersion() {
526

527
    if (this.ideHome != null) {
3!
528
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
529
      if (Files.exists(versionFile)) {
5✔
530
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
531
        return VersionIdentifier.of(version);
3✔
532
      }
533
    }
534
    return IdeMigrator.START_VERSION;
2✔
535
  }
536

537
  @Override
538
  public void setProjectVersion(VersionIdentifier version) {
539

540
    if (this.ideHome == null) {
3!
541
      throw new IllegalStateException("IDE_HOME not available!");
×
542
    }
543
    Objects.requireNonNull(version);
3✔
544
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
545
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
546
  }
1✔
547

548
  @Override
549
  public Path getIdeRoot() {
550

551
    return this.ideRoot;
3✔
552
  }
553

554
  @Override
555
  public Path getIdePath() {
556

557
    Path myIdeRoot = getIdeRoot();
3✔
558
    if (myIdeRoot == null) {
2✔
559
      return null;
2✔
560
    }
561
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
562
  }
563

564
  @Override
565
  public Path getCwd() {
566

567
    return this.cwd;
3✔
568
  }
569

570
  @Override
571
  public Path getTempPath() {
572

573
    Path idePath = getIdePath();
3✔
574
    if (idePath == null) {
2!
575
      return null;
×
576
    }
577
    return idePath.resolve("tmp");
4✔
578
  }
579

580
  @Override
581
  public Path getTempDownloadPath() {
582

583
    Path tmp = getTempPath();
3✔
584
    if (tmp == null) {
2!
585
      return null;
×
586
    }
587
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
588
  }
589

590
  @Override
591
  public Path getUserHome() {
592

593
    return this.userHome;
3✔
594
  }
595

596
  /**
597
   * This method should only be used for tests to mock user home.
598
   *
599
   * @param userHome the new value of {@link #getUserHome()}.
600
   */
601
  protected void setUserHome(Path userHome) {
602

603
    this.userHome = userHome;
3✔
604
    resetPrivacyMap();
2✔
605
  }
1✔
606

607
  @Override
608
  public Path getUserHomeIde() {
609

610
    return this.userHomeIde;
3✔
611
  }
612

613
  @Override
614
  public Path getSettingsPath() {
615

616
    return this.settingsPath;
3✔
617
  }
618

619
  @Override
620
  public Path getSettingsGitRepository() {
621

622
    Path settingsPath = getSettingsPath();
3✔
623
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
624
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
625
      LOG.error("Settings repository exists but is not a git repository.");
3✔
626
      return null;
2✔
627
    }
628
    return settingsPath;
2✔
629
  }
630

631
  @Override
632
  public boolean isSettingsRepositorySymlinkOrJunction() {
633

634
    Path settingsPath = getSettingsPath();
3✔
635
    if (settingsPath == null) {
2!
636
      return false;
×
637
    }
638
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
639
  }
640

641
  @Override
642
  public Path getSettingsCommitIdPath() {
643

644
    return this.settingsCommitIdPath;
3✔
645
  }
646

647
  @Override
648
  public Path getConfPath() {
649

650
    return this.confPath;
3✔
651
  }
652

653
  @Override
654
  public Path getSoftwarePath() {
655

656
    if (this.ideHome == null) {
3✔
657
      return null;
2✔
658
    }
659
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
660
  }
661

662
  @Override
663
  public Path getSoftwareExtraPath() {
664

665
    Path softwarePath = getSoftwarePath();
3✔
666
    if (softwarePath == null) {
2!
667
      return null;
×
668
    }
669
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
670
  }
671

672
  @Override
673
  public Path getSoftwareRepositoryPath() {
674

675
    Path idePath = getIdePath();
3✔
676
    if (idePath == null) {
2!
677
      return null;
×
678
    }
679
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
680
  }
681

682
  @Override
683
  public Path getPluginsPath() {
684

685
    return this.pluginsPath;
3✔
686
  }
687

688
  @Override
689
  public String getWorkspaceName() {
690

691
    return this.workspaceName;
3✔
692
  }
693

694
  @Override
695
  public Path getWorkspacesBasePath() {
696

697
    return this.workspacesBasePath;
3✔
698
  }
699

700
  @Override
701
  public Path getWorkspacePath() {
702

703
    return this.workspacePath;
3✔
704
  }
705

706
  @Override
707
  public Path getWorkspacePath(String workspace) {
708

709
    if (this.workspacesBasePath == null) {
3!
710
      throw new IllegalStateException("Failed to access workspace " + workspace + " without IDE_HOME in " + this.cwd);
×
711
    }
712
    return this.workspacesBasePath.resolve(workspace);
5✔
713
  }
714

715
  @Override
716
  public Path getDownloadPath() {
717

718
    return this.downloadPath;
3✔
719
  }
720

721
  @Override
722
  public Path getUrlsPath() {
723

724
    Path idePath = getIdePath();
3✔
725
    if (idePath == null) {
2!
726
      return null;
×
727
    }
728
    return idePath.resolve(FOLDER_URLS);
4✔
729
  }
730

731
  @Override
732
  public Path getToolRepositoryPath() {
733

734
    Path idePath = getIdePath();
3✔
735
    if (idePath == null) {
2!
736
      return null;
×
737
    }
738
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
739
  }
740

741
  @Override
742
  public SystemPath getPath() {
743

744
    return this.path;
3✔
745
  }
746

747
  @Override
748
  public EnvironmentVariables getVariables() {
749

750
    if (this.variables == null) {
3✔
751
      this.variables = createVariables();
4✔
752
    }
753
    return this.variables;
3✔
754
  }
755

756
  @Override
757
  public UrlMetadata getUrls() {
758

759
    if (this.urlMetadata == null) {
3✔
760
      if (!isTest()) {
3!
761
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
762
      }
763
      this.urlMetadata = new UrlMetadata(this);
6✔
764
    }
765
    return this.urlMetadata;
3✔
766
  }
767

768
  @Override
769
  public boolean isQuietMode() {
770

771
    return this.startContext.isQuietMode();
4✔
772
  }
773

774
  @Override
775
  public boolean isBatchMode() {
776

777
    return this.startContext.isBatchMode();
4✔
778
  }
779

780
  @Override
781
  public boolean isForceMode() {
782

783
    return this.startContext.isForceMode();
4✔
784
  }
785

786
  @Override
787
  public boolean isForcePull() {
788

789
    return this.startContext.isForcePull();
4✔
790
  }
791

792
  @Override
793
  public boolean isForcePlugins() {
794

795
    return this.startContext.isForcePlugins();
4✔
796
  }
797

798
  @Override
799
  public boolean isForceRepositories() {
800

801
    return this.startContext.isForceRepositories();
4✔
802
  }
803

804
  @Override
805
  public boolean isOfflineMode() {
806

807
    return this.startContext.isOfflineMode();
4✔
808
  }
809

810
  @Override
811
  public boolean isPrivacyMode() {
812
    return this.startContext.isPrivacyMode();
4✔
813
  }
814

815
  @Override
816
  public boolean isSkipUpdatesMode() {
817

818
    return this.startContext.isSkipUpdatesMode();
4✔
819
  }
820

821
  @Override
822
  public boolean isNoColorsMode() {
823

824
    return this.startContext.isNoColorsMode();
×
825
  }
826

827
  @Override
828
  public NetworkStatus getNetworkStatus() {
829

830
    if (this.networkStatus == null) {
×
831
      this.networkStatus = new NetworkStatusImpl(this);
×
832
    }
833
    return this.networkStatus;
×
834
  }
835

836
  @Override
837
  public Locale getLocale() {
838

839
    Locale locale = this.startContext.getLocale();
4✔
840
    if (locale == null) {
2✔
841
      locale = Locale.getDefault();
2✔
842
    }
843
    return locale;
2✔
844
  }
845

846
  @Override
847
  public DirectoryMerger getWorkspaceMerger() {
848

849
    if (this.workspaceMerger == null) {
3✔
850
      this.workspaceMerger = new DirectoryMerger(this);
6✔
851
    }
852
    return this.workspaceMerger;
3✔
853
  }
854

855
  /**
856
   * @return the default execution directory in which a command process is executed.
857
   */
858
  @Override
859
  public Path getDefaultExecutionDirectory() {
860

861
    return this.defaultExecutionDirectory;
×
862
  }
863

864
  /**
865
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
866
   */
867
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
868

869
    if (defaultExecutionDirectory != null) {
×
870
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
871
    }
872
  }
×
873

874
  @Override
875
  public GitContext getGitContext() {
876

877
    return new GitContextImpl(this);
×
878
  }
879

880
  @Override
881
  public ProcessContext newProcess() {
882

883
    ProcessContext processContext = createProcessContext();
3✔
884
    if (this.defaultExecutionDirectory != null) {
3!
885
      processContext.directory(this.defaultExecutionDirectory);
×
886
    }
887
    return processContext;
2✔
888
  }
889

890
  @Override
891
  public IdeSystem getSystem() {
892

893
    if (this.system == null) {
×
894
      this.system = new IdeSystemImpl();
×
895
    }
896
    return this.system;
×
897
  }
898

899
  /**
900
   * @return a new instance of {@link ProcessContext}.
901
   * @see #newProcess()
902
   */
903
  protected ProcessContext createProcessContext() {
904

905
    return new ProcessContextImpl(this);
×
906
  }
907

908
  @Override
909
  public IdeLogLevel getLogLevelConsole() {
910

911
    return this.startContext.getLogLevelConsole();
×
912
  }
913

914
  @Override
915
  public IdeLogLevel getLogLevelLogger() {
916

917
    return this.startContext.getLogLevelLogger();
×
918
  }
919

920
  @Override
921
  public IdeLogListener getLogListener() {
922

923
    return this.startContext.getLogListener();
×
924
  }
925

926
  @Override
927
  public void logIdeHomeAndRootStatus() {
928
    if (this.ideRoot != null) {
3!
929
      IdeLogLevel.SUCCESS.log(LOG, "IDE_ROOT is set to {}", this.ideRoot);
×
930
    }
931
    if (this.ideHome == null) {
3✔
932
      LOG.warn(getMessageNotInsideIdeProject());
5✔
933
    } else {
934
      IdeLogLevel.SUCCESS.log(LOG, "IDE_HOME is set to {}", this.ideHome);
11✔
935
    }
936
  }
1✔
937

938
  @Override
939
  public String formatArgument(Object argument) {
940

941
    if (argument == null) {
2✔
942
      return null;
2✔
943
    }
944
    String result = argument.toString();
3✔
945
    if (isPrivacyMode()) {
3✔
946
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
947
        initializePrivacyMap(this.userHome, "~");
×
948
        String projectName = getProjectName();
×
949
        if (!projectName.isEmpty()) {
×
950
          this.privacyMap.put(projectName, "project");
×
951
        }
952
      }
953
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
954
        result = result.replace(entry.getKey(), entry.getValue());
×
955
      }
×
956
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
957
    }
958
    return result;
2✔
959
  }
960

961
  /**
962
   * @param path the sensitive {@link Path} to
963
   * @param replacement the replacement to mask the {@link Path} in log output.
964
   */
965
  protected void initializePrivacyMap(Path path, String replacement) {
966

967
    if (path == null) {
×
968
      return;
×
969
    }
970
    if (this.systemInfo.isWindows()) {
×
971
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
972
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
973
    } else {
974
      this.privacyMap.put(path.toString(), replacement);
×
975
    }
976
  }
×
977

978
  /**
979
   * Resets the privacy map in case fundamental values have changed.
980
   */
981
  private void resetPrivacyMap() {
982

983
    this.privacyMap.clear();
3✔
984
  }
1✔
985

986

987
  @Override
988
  public String askForInput(String message, String defaultValue) {
989

990
    while (true) {
991
      if (!message.isBlank()) {
3!
992
        IdeLogLevel.INTERACTION.log(LOG, message);
4✔
993
      }
994
      if (isBatchMode()) {
3!
995
        if (isForceMode()) {
×
996
          return defaultValue;
×
997
        } else {
998
          throw new CliAbortException();
×
999
        }
1000
      }
1001
      String input = readLine().trim();
4✔
1002
      if (!input.isEmpty()) {
3!
1003
        return input;
2✔
1004
      } else {
1005
        if (defaultValue != null) {
×
1006
          return defaultValue;
×
1007
        }
1008
      }
1009
    }
×
1010
  }
1011

1012
  @Override
1013
  public <O> O question(O[] options, String question, Object... args) {
1014

1015
    assert (options.length > 0);
4!
1016
    IdeLogLevel.INTERACTION.log(LOG, question, args);
5✔
1017
    return displayOptionsAndGetAnswer(options);
4✔
1018
  }
1019

1020
  private <O> O displayOptionsAndGetAnswer(O[] options) {
1021
    if (options.length == 1) {
4✔
1022
      O option = options[0];
4✔
1023
      boolean vulnerable = false;
2✔
1024
      if (option instanceof com.devonfw.tools.ide.security.ToolVersionChoice choice) {
6✔
1025
        vulnerable = !choice.vulnerabilities().getIssues().isEmpty();
8!
1026
      }
1027
      if (!vulnerable) {
2✔
1028
        IdeLogLevel.INTERACTION.log(LOG, "" + option);
6✔
1029
        return option;
2✔
1030
      }
1031
    }
1032
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
1033
    int i = 0;
2✔
1034
    for (O option : options) {
16✔
1035
      i++;
1✔
1036
      String title = "" + option;
4✔
1037
      String key = computeOptionKey(title);
3✔
1038
      addMapping(mapping, key, option);
4✔
1039
      String numericKey = Integer.toString(i);
3✔
1040
      if (numericKey.equals(key)) {
4!
1041
        LOG.trace("Options should not be numeric: {}", key);
×
1042
      } else {
1043
        addMapping(mapping, numericKey, option);
4✔
1044
      }
1045
      IdeLogLevel.INTERACTION.log(LOG, "Option {}: {}", numericKey, title);
14✔
1046
    }
1047
    O option = null;
2✔
1048
    if (isBatchMode()) {
3!
1049
      if (isForceMode()) {
×
1050
        option = options[0];
×
1051
        IdeLogLevel.INTERACTION.log(LOG, "" + option);
×
1052
      }
1053
    } else {
1054
      while (option == null) {
2✔
1055
        String answer = readLine();
3✔
1056
        option = mapping.get(answer);
4✔
1057
        if (option == null) {
2!
1058
          LOG.warn("Invalid answer: '{}' - please try again.", answer);
×
1059
        }
1060
      }
1✔
1061
    }
1062
    return option;
2✔
1063
  }
1064

1065
  private static String computeOptionKey(String option) {
1066
    String key = option;
2✔
1067
    int index = -1;
2✔
1068
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
1069
      int currentIndex = key.indexOf(c);
4✔
1070
      if (currentIndex != -1) {
3✔
1071
        if ((index == -1) || (currentIndex < index)) {
3!
1072
          index = currentIndex;
2✔
1073
        }
1074
      }
1075
    }
1076
    if (index > 0) {
2✔
1077
      key = key.substring(0, index).trim();
6✔
1078
    }
1079
    return key;
2✔
1080
  }
1081

1082
  /**
1083
   * @return the input from the end-user (e.g. read from the console).
1084
   */
1085
  protected abstract String readLine();
1086

1087
  private static <O> void addMapping(Map<String, O> mapping, String key, O option) {
1088

1089
    O duplicate = mapping.put(key, option);
5✔
1090
    if (duplicate != null) {
2!
1091
      throw new IllegalArgumentException("Duplicated option " + key);
×
1092
    }
1093
  }
1✔
1094

1095
  @Override
1096
  public Step getCurrentStep() {
1097

1098
    return this.currentStep;
×
1099
  }
1100

1101
  @Override
1102
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1103

1104
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1105
    return this.currentStep;
3✔
1106
  }
1107

1108
  /**
1109
   * Internal method to end the running {@link Step}.
1110
   *
1111
   * @param step the current {@link Step} to end.
1112
   */
1113
  public void endStep(StepImpl step) {
1114

1115
    if (step == this.currentStep) {
4!
1116
      this.currentStep = this.currentStep.getParent();
6✔
1117
    } else {
1118
      String currentStepName = "null";
×
1119
      if (this.currentStep != null) {
×
1120
        currentStepName = this.currentStep.getName();
×
1121
      }
1122
      LOG.warn("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1123
    }
1124
  }
1✔
1125

1126
  /**
1127
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1128
   *
1129
   * @param arguments the {@link CliArgument}.
1130
   * @return the return code of the execution.
1131
   */
1132
  public int run(CliArguments arguments) {
1133

1134
    CliArgument current = arguments.current();
3✔
1135
    assert (this.currentStep == null);
4!
1136
    boolean supressStepSuccess = false;
2✔
1137
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1138
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1139
    Commandlet cmd = null;
2✔
1140
    ValidationResult result = null;
2✔
1141
    try {
1142
      while (commandletIterator.hasNext()) {
3✔
1143
        cmd = commandletIterator.next();
4✔
1144
        result = applyAndRun(arguments.copy(), cmd);
6✔
1145
        if (result.isValid()) {
3!
1146
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1147
          step.success();
2✔
1148
          return ProcessResult.SUCCESS;
4✔
1149
        }
1150
      }
1151
      activateLogging(cmd);
3✔
1152
      verifyIdeMinVersion(false);
3✔
1153
      if (result != null) {
2!
1154
        LOG.error(result.getErrorMessage());
×
1155
      }
1156
      step.error("Invalid arguments: {}", current.getArgs());
10✔
1157
      IdeLogLevel.INTERACTION.log(LOG, "For additional details run ide help {}", cmd == null ? "" : cmd.getName());
13!
1158
      return 1;
4✔
1159
    } catch (Throwable t) {
1✔
1160
      activateLogging(cmd);
3✔
1161
      step.error(t, true);
4✔
1162
      if (this.logfile != null) {
3!
1163
        System.err.println("Logfile can be found at " + this.logfile); // do not use logger
×
1164
      }
1165
      throw t;
2✔
1166
    } finally {
1167
      step.close();
2✔
1168
      assert (this.currentStep == null);
4!
1169
      step.logSummary(supressStepSuccess);
3✔
1170
    }
1171
  }
1172

1173
  /**
1174
   * Ensure the logging system is initialized.
1175
   */
1176
  private void activateLogging(Commandlet cmd) {
1177

1178
    configureJavaUtilLogging(cmd);
3✔
1179
    this.startContext.activateLogging();
3✔
1180
  }
1✔
1181

1182
  /**
1183
   * Configures the logging system (JUL).
1184
   *
1185
   * @param cmd the {@link Commandlet} to be called. May be {@code null}.
1186
   */
1187
  public void configureJavaUtilLogging(Commandlet cmd) {
1188

1189
    if (this.julConfigured) {
3✔
1190
      return;
1✔
1191
    }
1192
    boolean writeLogfile = isWriteLogfile(cmd);
4✔
1193
    this.startContext.setWriteLogfile(writeLogfile);
4✔
1194
    Properties properties = createJavaUtilLoggingProperties(writeLogfile, cmd);
5✔
1195
    try {
1196
      ByteArrayOutputStream out = new ByteArrayOutputStream(512);
5✔
1197
      properties.store(out, null);
4✔
1198
      out.flush();
2✔
1199
      ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
6✔
1200
      LogManager.getLogManager().readConfiguration(in);
3✔
1201
      this.julConfigured = true;
3✔
1202
      this.startContext.activateLogging();
3✔
1203
    } catch (IOException e) {
×
1204
      LOG.error("Failed to configure logging: {}", e.toString(), e);
×
1205
    }
1✔
1206
  }
1✔
1207

1208
  protected boolean isWriteLogfile(Commandlet cmd) {
1209
    if ((cmd == null) || !cmd.isWriteLogFile()) {
×
1210
      return false;
×
1211
    }
1212
    Boolean writeLogfile = IdeVariables.IDE_WRITE_LOGFILE.get(this);
×
1213
    return Boolean.TRUE.equals(writeLogfile);
×
1214
  }
1215

1216
  private Properties createJavaUtilLoggingProperties(boolean writeLogfile, Commandlet cmd) {
1217

1218
    Path idePath = getIdePath();
3✔
1219
    if (writeLogfile && (idePath == null)) {
2!
1220
      writeLogfile = false;
×
1221
      LOG.error("Cannot enable log-file since IDE_ROOT is undefined.");
×
1222
    }
1223
    Properties properties = new Properties();
4✔
1224
    // prevent 3rd party (e.g. java.lang.ProcessBuilder) logging into our console via JUL
1225
    // see JulLogLevel for the trick we did to workaround JUL flaws
1226
    properties.setProperty(".level", "SEVERE");
5✔
1227
    if (writeLogfile) {
2!
1228
      this.startContext.setLogLevelLogger(IdeLogLevel.TRACE);
×
1229
      String fileHandlerName = FileHandler.class.getName();
×
1230
      properties.setProperty("handlers", JulConsoleHandler.class.getName() + "," + fileHandlerName);
×
1231
      properties.setProperty(fileHandlerName + ".formatter", SimpleFormatter.class.getName());
×
1232
      properties.setProperty(fileHandlerName + ".encoding", "UTF-8");
×
1233
      this.logfile = createLogfilePath(idePath, cmd);
×
1234
      getFileAccess().mkdirs(this.logfile.getParent());
×
1235
      properties.setProperty(fileHandlerName + ".pattern", this.logfile.toString());
×
1236
    } else {
×
1237
      properties.setProperty("handlers", JulConsoleHandler.class.getName());
6✔
1238
    }
1239
    properties.setProperty(SimpleFormatter.class.getName() + ".format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL [%4$s] [%3$s] %5$s%6$s%n");
7✔
1240
    return properties;
2✔
1241
  }
1242

1243
  private Path createLogfilePath(Path idePath, Commandlet cmd) {
1244
    LocalDateTime now = LocalDateTime.now();
×
1245
    Path logsPath = idePath.resolve(FOLDER_LOGS).resolve(DateTimeUtil.formatDate(now, true));
×
1246
    StringBuilder sb = new StringBuilder(32);
×
1247
    if (this.ideHome == null || ((cmd != null) && !cmd.isIdeHomeRequired())) {
×
1248
      sb.append("_ide-");
×
1249
    } else {
1250
      sb.append(this.ideHome.getFileName().toString());
×
1251
      sb.append('-');
×
1252
    }
1253
    sb.append("ide-");
×
1254
    if (cmd != null) {
×
1255
      sb.append(cmd.getName());
×
1256
      sb.append('-');
×
1257
    }
1258
    sb.append(DateTimeUtil.formatTime(now));
×
1259
    sb.append(".log");
×
1260
    return logsPath.resolve(sb.toString());
×
1261
  }
1262

1263
  @Override
1264
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1265

1266
    this.startContext.deactivateLogging(threshold);
4✔
1267
    lambda.run();
2✔
1268
    this.startContext.activateLogging();
3✔
1269
  }
1✔
1270

1271
  /**
1272
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1273
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1274
   *     {@link Commandlet} did not match and we have to try a different candidate).
1275
   */
1276
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1277

1278
    IdeLogLevel previousLogLevel = null;
2✔
1279
    cmd.reset();
2✔
1280
    ValidationResult result = apply(arguments, cmd);
5✔
1281
    if (result.isValid()) {
3!
1282
      result = cmd.validate();
3✔
1283
    }
1284
    if (result.isValid()) {
3!
1285
      LOG.debug("Running commandlet {}", cmd);
4✔
1286
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1287
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1288
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1289
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1290
      }
1291
      try {
1292
        if (cmd.isProcessableOutput()) {
3!
1293
          if (!LOG.isDebugEnabled()) {
×
1294
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1295
            previousLogLevel = this.startContext.setLogLevelConsole(IdeLogLevel.PROCESSABLE);
×
1296
          }
1297
        } else {
1298
          if (cmd.isIdeHomeRequired()) {
3!
1299
            LOG.debug(getMessageIdeHomeFound());
4✔
1300
          }
1301
          Path settingsRepository = getSettingsGitRepository();
3✔
1302
          if (settingsRepository != null) {
2!
1303
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1304
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1305
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1306
              String msg;
1307
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1308
                msg = "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.";
×
1309
              } else {
1310
                msg = "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"";
×
1311
              }
1312
              IdeLogLevel.INTERACTION.log(LOG, msg);
×
1313
            }
1314
          }
1315
        }
1316
        boolean success = ensureLicenseAgreement(cmd);
4✔
1317
        if (!success) {
2!
1318
          return ValidationResultValid.get();
×
1319
        }
1320
        cmd.run();
2✔
1321
      } finally {
1322
        if (previousLogLevel != null) {
2!
1323
          this.startContext.setLogLevelConsole(previousLogLevel);
×
1324
        }
1325
      }
1✔
1326
    } else {
1327
      LOG.trace("Commandlet did not match");
×
1328
    }
1329
    return result;
2✔
1330
  }
1331

1332
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1333

1334
    if (isTest()) {
3!
1335
      return true; // ignore for tests
2✔
1336
    }
1337
    getFileAccess().mkdirs(this.userHomeIde);
×
1338
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1339
    if (Files.isRegularFile(licenseAgreement)) {
×
1340
      return true; // success, license already accepted
×
1341
    }
1342
    if (cmd instanceof EnvironmentCommandlet) {
×
1343
      // if the license was not accepted, "$(ideasy env --bash)" that is written into a variable prevents the user from seeing the question he is asked
1344
      // in such situation the user could not open a bash terminal anymore and gets blocked what would really annoy the user so we exit here without doing or
1345
      // printing anything anymore in such case.
1346
      return false;
×
1347
    }
1348
    activateLogging(cmd);
×
1349
    IdeLogLevel oldLogLevel = this.startContext.getLogLevelConsole();
×
1350
    IdeLogLevel newLogLevel = oldLogLevel;
×
1351
    if (oldLogLevel.ordinal() > IdeLogLevel.INFO.ordinal()) {
×
1352
      newLogLevel = IdeLogLevel.INFO;
×
1353
      this.startContext.setLogLevelConsole(newLogLevel);
×
1354
    }
1355
    StringBuilder sb = new StringBuilder(1180);
×
1356
    sb.append(LOGO).append("""
×
1357
        Welcome to IDEasy!
1358
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1359
        It supports automatic download and installation of arbitrary 3rd party tools.
1360
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1361
        But if explicitly configured, also commercial software that requires an additional license may be used.
1362
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1363
        You are solely responsible for all risks implied by using this software.
1364
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1365
        You will be able to find it online under the following URL:
1366
        """).append(LICENSE_URL);
×
1367
    if (this.ideRoot != null) {
×
1368
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1369
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1370
    }
1371
    LOG.info(sb.toString());
×
1372
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1373

1374
    sb.setLength(0);
×
1375
    LocalDateTime now = LocalDateTime.now();
×
1376
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1377
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1378
    try {
1379
      Files.writeString(licenseAgreement, sb);
×
1380
    } catch (Exception e) {
×
1381
      throw new RuntimeException("Failed to save license agreement!", e);
×
1382
    }
×
1383
    if (oldLogLevel != newLogLevel) {
×
1384
      this.startContext.setLogLevelConsole(oldLogLevel);
×
1385
    }
1386
    return true;
×
1387
  }
1388

1389
  @Override
1390
  public void verifyIdeMinVersion(boolean throwException) {
1391
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1392
    if (minVersion == null) {
2✔
1393
      return;
1✔
1394
    }
1395
    VersionIdentifier versionIdentifier = IdeVersion.getVersionIdentifier();
2✔
1396
    if (versionIdentifier.compareVersion(minVersion).isLess() && !IdeVersion.isUndefined()) {
7!
1397
      String message = String.format("Your version of IDEasy is currently %s\n"
13✔
1398
          + "However, this is too old as your project requires at latest version %s\n"
1399
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1400
          + "ide upgrade", versionIdentifier, minVersion);
1401
      if (throwException) {
2✔
1402
        throw new CliException(message);
5✔
1403
      } else {
1404
        LOG.warn(message);
3✔
1405
      }
1406
    }
1407
  }
1✔
1408

1409
  /**
1410
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1411
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1412
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1413
   */
1414
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1415

1416
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1417
    if (arguments.current().isStart()) {
4✔
1418
      arguments.next();
3✔
1419
    }
1420
    if (includeContextOptions) {
2✔
1421
      ContextCommandlet cc = new ContextCommandlet();
4✔
1422
      for (Property<?> property : cc.getProperties()) {
11✔
1423
        assert (property.isOption());
4!
1424
        property.apply(arguments, this, cc, collector);
7✔
1425
      }
1✔
1426
    }
1427
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1428
    CliArgument current = arguments.current();
3✔
1429
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1430
      collector.add(current.get(), null, null, null);
7✔
1431
    }
1432
    arguments.next();
3✔
1433
    while (commandletIterator.hasNext()) {
3✔
1434
      Commandlet cmd = commandletIterator.next();
4✔
1435
      if (!arguments.current().isEnd()) {
4✔
1436
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1437
      }
1438
    }
1✔
1439
    return collector.getSortedCandidates();
3✔
1440
  }
1441

1442
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1443

1444
    LOG.trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
5✔
1445
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1446
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1447
    List<Property<?>> properties = cmd.getProperties();
3✔
1448
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1449
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1450
    for (Property<?> property : properties) {
10✔
1451
      if (property.isOption()) {
3✔
1452
        optionProperties.add(property);
4✔
1453
      }
1454
    }
1✔
1455
    CliArgument currentArgument = arguments.current();
3✔
1456
    while (!currentArgument.isEnd()) {
3✔
1457
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1458
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1459
        if (currentArgument.isCompletion()) {
3✔
1460
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1461
          while (optionIterator.hasNext()) {
3✔
1462
            Property<?> option = optionIterator.next();
4✔
1463
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1464
            if (success) {
2✔
1465
              optionIterator.remove();
2✔
1466
              arguments.next();
3✔
1467
            }
1468
          }
1✔
1469
        } else {
1✔
1470
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1471
          if (option != null) {
2✔
1472
            arguments.next();
3✔
1473
            boolean removed = optionProperties.remove(option);
4✔
1474
            if (!removed) {
2!
1475
              option = null;
×
1476
            }
1477
          }
1478
          if (option == null) {
2✔
1479
            LOG.trace("No such option was found.");
3✔
1480
            return;
1✔
1481
          }
1482
        }
1✔
1483
      } else {
1484
        if (valueIterator.hasNext()) {
3✔
1485
          Property<?> valueProperty = valueIterator.next();
4✔
1486
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1487
          if (!success) {
2✔
1488
            LOG.trace("Completion cannot match any further.");
3✔
1489
            return;
1✔
1490
          }
1491
        } else {
1✔
1492
          LOG.trace("No value left for completion.");
3✔
1493
          return;
1✔
1494
        }
1495
      }
1496
      currentArgument = arguments.current();
4✔
1497
    }
1498
  }
1✔
1499

1500
  /**
1501
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1502
   *     {@link CliArguments#copy() copy} as needed.
1503
   * @param cmd the potential {@link Commandlet} to match.
1504
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1505
   */
1506
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1507

1508
    LOG.trace("Trying to match arguments to commandlet {}", cmd.getName());
5✔
1509
    CliArgument currentArgument = arguments.current();
3✔
1510
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1511
    Property<?> property = null;
2✔
1512
    if (propertyIterator.hasNext()) {
3!
1513
      property = propertyIterator.next();
4✔
1514
    }
1515
    while (!currentArgument.isEnd()) {
3✔
1516
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1517
      Property<?> currentProperty = property;
2✔
1518
      if (!arguments.isEndOptions()) {
3!
1519
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1520
        if (option != null) {
2!
1521
          currentProperty = option;
×
1522
        }
1523
      }
1524
      if (currentProperty == null) {
2!
1525
        LOG.trace("No option or next value found");
×
1526
        ValidationState state = new ValidationState(null);
×
1527
        state.addErrorMessage("No matching property found");
×
1528
        return state;
×
1529
      }
1530
      LOG.trace("Next property candidate to match argument is {}", currentProperty);
4✔
1531
      if (currentProperty == property) {
3!
1532
        if (!property.isMultiValued()) {
3✔
1533
          if (propertyIterator.hasNext()) {
3✔
1534
            property = propertyIterator.next();
5✔
1535
          } else {
1536
            property = null;
2✔
1537
          }
1538
        }
1539
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1540
          arguments.stopSplitShortOptions();
2✔
1541
        }
1542
      }
1543
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1544
      if (!matches) {
2!
1545
        ValidationState state = new ValidationState(null);
×
1546
        state.addErrorMessage("No matching property found");
×
1547
        return state;
×
1548
      }
1549
      currentArgument = arguments.current();
3✔
1550
    }
1✔
1551
    return ValidationResultValid.get();
2✔
1552
  }
1553

1554
  @Override
1555
  public Path findBash() {
1556
    if (this.bash != null) {
3✔
1557
      return this.bash;
3✔
1558
    }
1559
    Path bashPath = findBashOnBashPath();
3✔
1560
    if (bashPath == null) {
2✔
1561
      bashPath = findBashInPath();
3✔
1562
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1563
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1564
        if (bashPath == null) {
2!
1565
          bashPath = findBashInWindowsRegistry();
3✔
1566
        }
1567
      }
1568
    }
1569
    if (bashPath == null) {
2✔
1570
      LOG.error("No bash executable could be found on your system.");
4✔
1571
    } else {
1572
      this.bash = bashPath;
3✔
1573
    }
1574
    return bashPath;
2✔
1575
  }
1576

1577
  private Path findBashOnBashPath() {
1578
    LOG.trace("Trying to find BASH_PATH environment variable.");
3✔
1579
    Path bash;
1580
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1581
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1582
    if (bashVariable != null) {
2✔
1583
      bash = Path.of(bashVariable);
5✔
1584
      if (Files.exists(bash)) {
5✔
1585
        LOG.debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
5✔
1586
        return bash;
2✔
1587
      } else {
1588
        LOG.error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
5✔
1589
        return null;
2✔
1590
      }
1591
    } else {
1592
      LOG.debug("{} environment variable was not found", bashPathVariableName);
4✔
1593
      return null;
2✔
1594
    }
1595
  }
1596

1597
  /**
1598
   * @param path the path to check.
1599
   * @param toIgnore the String sequence which needs to be checked and ignored.
1600
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1601
   */
1602
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1603
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1604
    return !s.contains(toIgnore);
7!
1605
  }
1606

1607
  /**
1608
   * Tries to find the bash.exe within the PATH environment variable.
1609
   *
1610
   * @return Path to bash.exe if found in PATH environment variable, {@code null} if bash.exe was not found.
1611
   */
1612
  private Path findBashInPath() {
1613
    LOG.trace("Trying to find bash in PATH environment variable.");
3✔
1614
    Path bash;
1615
    String pathVariableName = IdeVariables.PATH.getName();
3✔
1616
    if (pathVariableName != null) {
2!
1617
      Path plainBash = Path.of(BASH);
5✔
1618
      Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
16!
1619
          "\\windows\\system32");
1620
      Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
6✔
1621
      bash = bashPath.toAbsolutePath();
3✔
1622
      if (bashPath.equals(plainBash)) {
4✔
1623
        LOG.warn("No usable bash executable was found in your PATH environment variable!");
3✔
1624
        bash = null;
3✔
1625
      } else {
1626
        if (Files.exists(bashPath)) {
5!
1627
          LOG.debug("A proper bash executable was found in your PATH environment variable at: {}", bash);
5✔
1628
        } else {
1629
          bash = null;
×
1630
          LOG.error("A path to a bash executable was found in your PATH environment variable at: {} but the file is not existing.", bash);
×
1631
        }
1632
      }
1633
    } else {
1✔
1634
      bash = null;
×
1635
      // this should never happen...
1636
      LOG.error("PATH environment variable was not found");
×
1637
    }
1638
    return bash;
2✔
1639
  }
1640

1641
  /**
1642
   * Tries to find the bash.exe within the Windows registry.
1643
   *
1644
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1645
   */
1646
  protected Path findBashInWindowsRegistry() {
1647
    LOG.trace("Trying to find bash in Windows registry");
×
1648
    // If not found in the default location, try the registry query
1649
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1650
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1651
    for (String bashVariant : bashVariants) {
×
1652
      LOG.trace("Trying to find bash variant: {}", bashVariant);
×
1653
      for (String registryKey : registryKeys) {
×
1654
        LOG.trace("Trying to find bash from registry key: {}", registryKey);
×
1655
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1656
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1657

1658
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1659
        if (path != null) {
×
1660
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1661
          if (Files.exists(bashPath)) {
×
1662
            LOG.debug("Found bash at: {}", bashPath);
×
1663
            return bashPath;
×
1664
          } else {
1665
            LOG.error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1666
            return null;
×
1667
          }
1668
        } else {
1669
          LOG.info("No bash executable could be found in the Windows registry.");
×
1670
        }
1671
      }
1672
    }
1673
    // no bash found
1674
    return null;
×
1675
  }
1676

1677
  private Path findBashOnWindowsDefaultGitPath() {
1678
    // Check if Git Bash exists in the default location
1679
    LOG.trace("Trying to find bash on the Windows default git path.");
3✔
1680
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1681
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1682
      LOG.trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1683
      return defaultPath;
×
1684
    }
1685
    LOG.debug("No bash was found on the Windows default git path.");
3✔
1686
    return null;
2✔
1687
  }
1688

1689
  @Override
1690
  public WindowsPathSyntax getPathSyntax() {
1691

1692
    return this.pathSyntax;
3✔
1693
  }
1694

1695
  /**
1696
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1697
   */
1698
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1699

1700
    this.pathSyntax = pathSyntax;
3✔
1701
  }
1✔
1702

1703
  /**
1704
   * @return the {@link IdeStartContextImpl}.
1705
   */
1706
  public IdeStartContextImpl getStartContext() {
1707

1708
    return startContext;
3✔
1709
  }
1710

1711
  /**
1712
   * @return the {@link WindowsHelper}.
1713
   */
1714
  public final WindowsHelper getWindowsHelper() {
1715

1716
    if (this.windowsHelper == null) {
3✔
1717
      this.windowsHelper = createWindowsHelper();
4✔
1718
    }
1719
    return this.windowsHelper;
3✔
1720
  }
1721

1722
  /**
1723
   * @return the new {@link WindowsHelper} instance.
1724
   */
1725
  protected WindowsHelper createWindowsHelper() {
1726

1727
    return new WindowsHelperImpl(this);
×
1728
  }
1729

1730
  /**
1731
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1732
   */
1733
  public void reload() {
1734

1735
    this.variables = null;
3✔
1736
    this.customToolRepository = null;
3✔
1737
  }
1✔
1738

1739
  @Override
1740
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1741

1742
    assert (Files.isDirectory(installationPath));
6!
1743
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1744
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1745
  }
1✔
1746

1747
  /*
1748
   * @param home the IDE_HOME directory.
1749
   * @param workspace the name of the active workspace folder.
1750
   */
1751
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1752

1753
  }
1754

1755
  /**
1756
   * Returns the default git path on Windows. Required to be overwritten in tests.
1757
   *
1758
   * @return default path to git on Windows.
1759
   */
1760
  public String getDefaultWindowsGitPath() {
1761
    return DEFAULT_WINDOWS_GIT_PATH;
×
1762
  }
1763

1764
}
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