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

devonfw / IDEasy / 22303886886

23 Feb 2026 11:19AM UTC coverage: 70.647% (+0.2%) from 70.474%
22303886886

Pull #1714

github

web-flow
Merge f1f7e1e61 into 379acdc9d
Pull Request #1714: #404: #1713: advanced logging

4069 of 6360 branches covered (63.98%)

Branch coverage included in aggregate %.

10644 of 14466 relevant lines covered (73.58%)

3.1 hits per line

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

66.51
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.LogManager;
22

23
import org.slf4j.Logger;
24
import org.slf4j.LoggerFactory;
25

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

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

90
  static final Logger LOG = LoggerFactory.getLogger(AbstractIdeContext.class);
3✔
91

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

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

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

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

101
  private static final String OPTION_DETAILS_START = "([";
102

103
  private final IdeStartContextImpl startContext;
104

105
  private Path ideHome;
106

107
  private final Path ideRoot;
108

109
  private Path confPath;
110

111
  protected Path settingsPath;
112

113
  private Path settingsCommitIdPath;
114

115
  protected Path pluginsPath;
116

117
  private Path workspacePath;
118

119
  private String workspaceName;
120

121
  private Path cwd;
122

123
  private Path downloadPath;
124

125
  private Path userHome;
126

127
  private Path userHomeIde;
128

129
  private SystemPath path;
130

131
  private WindowsPathSyntax pathSyntax;
132

133
  private final SystemInfo systemInfo;
134

135
  private EnvironmentVariables variables;
136

137
  private final FileAccess fileAccess;
138

139
  protected CommandletManager commandletManager;
140

141
  protected ToolRepository defaultToolRepository;
142

143
  private CustomToolRepository customToolRepository;
144

145
  private MvnRepository mvnRepository;
146

147
  private NpmRepository npmRepository;
148

149
  private PipRepository pipRepository;
150

151
  private DirectoryMerger workspaceMerger;
152

153
  protected UrlMetadata urlMetadata;
154

155
  protected Path defaultExecutionDirectory;
156

157
  private StepImpl currentStep;
158

159
  private NetworkStatus networkStatus;
160

161
  protected IdeSystem system;
162

163
  private WindowsHelper windowsHelper;
164

165
  private final Map<String, String> privacyMap;
166

167
  private Path bash;
168

169
  private boolean julConfigured;
170

171
  private Path logfile;
172

173
  /**
174
   * The constructor.
175
   *
176
   * @param startContext the {@link IdeStartContextImpl}.
177
   * @param workingDirectory the optional {@link Path} to current working directory.
178
   */
179
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
180

181
    super();
2✔
182

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

212
    // detection completed, initializing variables
213
    this.ideRoot = findIdeRoot(ideHomeDir);
5✔
214

215
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
216

217
    if (this.ideRoot != null) {
3✔
218
      Path tempDownloadPath = getTempDownloadPath();
3✔
219
      if (Files.isDirectory(tempDownloadPath)) {
6✔
220
        // TODO delete all files older than 1 day here...
221
      } else {
222
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
223
      }
224
    }
225
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
226
  }
1✔
227

228
  /**
229
   * 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
230
   * add additional validation or boundary checks.
231
   *
232
   * @param workingDirectory the starting directory for the search.
233
   * @return an instance of {@link IdeHomeAndWorkspace} where the IDE_HOME was found or {@code null} if not found.
234
   */
235
  protected IdeHomeAndWorkspace findIdeHome(Path workingDirectory) {
236

237
    Path currentDir = workingDirectory;
2✔
238
    String name1 = "";
2✔
239
    String name2 = "";
2✔
240
    String workspace = WORKSPACE_MAIN;
2✔
241
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
242

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

263
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
264
  }
265

266
  /**
267
   * @return a new {@link MvnRepository}
268
   */
269
  protected MvnRepository createMvnRepository() {
270
    return new MvnRepository(this);
5✔
271
  }
272

273
  /**
274
   * @return a new {@link NpmRepository}
275
   */
276
  protected NpmRepository createNpmRepository() {
277
    return new NpmRepository(this);
×
278
  }
279

280
  /**
281
   * @return a new {@link PipRepository}
282
   */
283
  protected PipRepository createPipRepository() {
284
    return new PipRepository(this);
×
285
  }
286

287
  private Path findIdeRoot(Path ideHomePath) {
288

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

306
  /**
307
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
308
   */
309
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
310

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

343
  @Override
344
  public void setCwd(Path userDir, String workspace, Path ideHome) {
345

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

375
  private String getMessageIdeHomeFound() {
376

377
    String wks = this.workspaceName;
3✔
378
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
379
      wks = "*".repeat(wks.length());
×
380
    }
381
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
382
  }
383

384
  private String getMessageNotInsideIdeProject() {
385

386
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
387
  }
388

389
  private String getMessageIdeRootNotFound() {
390

391
    String root = getSystem().getEnv("IDE_ROOT");
5✔
392
    if (root == null) {
2!
393
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
394
    } else {
395
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
396
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
397
    }
398
  }
399

400
  /**
401
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
402
   */
403
  public boolean isTest() {
404

405
    return false;
×
406
  }
407

408
  protected SystemPath computeSystemPath() {
409

410
    return new SystemPath(this);
×
411
  }
412

413
  /**
414
   * Checks if the given directory is a valid IDE home by verifying it contains both 'workspaces' and 'settings' directories.
415
   *
416
   * @param dir the directory to check.
417
   * @return {@code true} if the directory is a valid IDE home, {@code false} otherwise.
418
   */
419
  protected boolean isIdeHome(Path dir) {
420

421
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
422
      return false;
2✔
423
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
424
      return false;
×
425
    }
426
    return true;
2✔
427
  }
428

429
  private EnvironmentVariables createVariables() {
430

431
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
432
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
433
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
434
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
435
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
436
    return conf.resolved();
3✔
437
  }
438

439
  protected AbstractEnvironmentVariables createSystemVariables() {
440

441
    return EnvironmentVariables.ofSystem(this);
3✔
442
  }
443

444
  @Override
445
  public SystemInfo getSystemInfo() {
446

447
    return this.systemInfo;
3✔
448
  }
449

450
  @Override
451
  public FileAccess getFileAccess() {
452

453
    return this.fileAccess;
3✔
454
  }
455

456
  @Override
457
  public CommandletManager getCommandletManager() {
458

459
    return this.commandletManager;
3✔
460
  }
461

462
  @Override
463
  public ToolRepository getDefaultToolRepository() {
464

465
    return this.defaultToolRepository;
3✔
466
  }
467

468
  @Override
469
  public MvnRepository getMvnRepository() {
470
    if (this.mvnRepository == null) {
3✔
471
      this.mvnRepository = createMvnRepository();
4✔
472
    }
473
    return this.mvnRepository;
3✔
474
  }
475

476
  @Override
477
  public NpmRepository getNpmRepository() {
478
    if (this.npmRepository == null) {
3✔
479
      this.npmRepository = createNpmRepository();
4✔
480
    }
481
    return this.npmRepository;
3✔
482
  }
483

484
  @Override
485
  public PipRepository getPipRepository() {
486
    if (this.pipRepository == null) {
3✔
487
      this.pipRepository = createPipRepository();
4✔
488
    }
489
    return this.pipRepository;
3✔
490
  }
491

492
  @Override
493
  public CustomToolRepository getCustomToolRepository() {
494

495
    if (this.customToolRepository == null) {
3!
496
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
497
    }
498
    return this.customToolRepository;
3✔
499
  }
500

501
  @Override
502
  public Path getIdeHome() {
503

504
    return this.ideHome;
3✔
505
  }
506

507
  @Override
508
  public String getProjectName() {
509

510
    if (this.ideHome != null) {
3!
511
      return this.ideHome.getFileName().toString();
5✔
512
    }
513
    return "";
×
514
  }
515

516
  @Override
517
  public VersionIdentifier getProjectVersion() {
518

519
    if (this.ideHome != null) {
3!
520
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
521
      if (Files.exists(versionFile)) {
5✔
522
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
523
        return VersionIdentifier.of(version);
3✔
524
      }
525
    }
526
    return IdeMigrator.START_VERSION;
2✔
527
  }
528

529
  @Override
530
  public void setProjectVersion(VersionIdentifier version) {
531

532
    if (this.ideHome == null) {
3!
533
      throw new IllegalStateException("IDE_HOME not available!");
×
534
    }
535
    Objects.requireNonNull(version);
3✔
536
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
537
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
538
  }
1✔
539

540
  @Override
541
  public Path getIdeRoot() {
542

543
    return this.ideRoot;
3✔
544
  }
545

546
  @Override
547
  public Path getIdePath() {
548

549
    Path myIdeRoot = getIdeRoot();
3✔
550
    if (myIdeRoot == null) {
2✔
551
      return null;
2✔
552
    }
553
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
554
  }
555

556
  @Override
557
  public Path getCwd() {
558

559
    return this.cwd;
3✔
560
  }
561

562
  @Override
563
  public Path getTempPath() {
564

565
    Path idePath = getIdePath();
3✔
566
    if (idePath == null) {
2!
567
      return null;
×
568
    }
569
    return idePath.resolve("tmp");
4✔
570
  }
571

572
  @Override
573
  public Path getTempDownloadPath() {
574

575
    Path tmp = getTempPath();
3✔
576
    if (tmp == null) {
2!
577
      return null;
×
578
    }
579
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
580
  }
581

582
  @Override
583
  public Path getUserHome() {
584

585
    return this.userHome;
3✔
586
  }
587

588
  /**
589
   * This method should only be used for tests to mock user home.
590
   *
591
   * @param userHome the new value of {@link #getUserHome()}.
592
   */
593
  protected void setUserHome(Path userHome) {
594

595
    this.userHome = userHome;
3✔
596
    resetPrivacyMap();
2✔
597
  }
1✔
598

599
  @Override
600
  public Path getUserHomeIde() {
601

602
    return this.userHomeIde;
3✔
603
  }
604

605
  @Override
606
  public Path getSettingsPath() {
607

608
    return this.settingsPath;
3✔
609
  }
610

611
  @Override
612
  public Path getSettingsGitRepository() {
613

614
    Path settingsPath = getSettingsPath();
3✔
615
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
616
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
617
      LOG.error("Settings repository exists but is not a git repository.");
3✔
618
      return null;
2✔
619
    }
620
    return settingsPath;
2✔
621
  }
622

623
  @Override
624
  public boolean isSettingsRepositorySymlinkOrJunction() {
625

626
    Path settingsPath = getSettingsPath();
3✔
627
    if (settingsPath == null) {
2!
628
      return false;
×
629
    }
630
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
631
  }
632

633
  @Override
634
  public Path getSettingsCommitIdPath() {
635

636
    return this.settingsCommitIdPath;
3✔
637
  }
638

639
  @Override
640
  public Path getConfPath() {
641

642
    return this.confPath;
3✔
643
  }
644

645
  @Override
646
  public Path getSoftwarePath() {
647

648
    if (this.ideHome == null) {
3✔
649
      return null;
2✔
650
    }
651
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
652
  }
653

654
  @Override
655
  public Path getSoftwareExtraPath() {
656

657
    Path softwarePath = getSoftwarePath();
3✔
658
    if (softwarePath == null) {
2!
659
      return null;
×
660
    }
661
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
662
  }
663

664
  @Override
665
  public Path getSoftwareRepositoryPath() {
666

667
    Path idePath = getIdePath();
3✔
668
    if (idePath == null) {
2!
669
      return null;
×
670
    }
671
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
672
  }
673

674
  @Override
675
  public Path getPluginsPath() {
676

677
    return this.pluginsPath;
3✔
678
  }
679

680
  @Override
681
  public String getWorkspaceName() {
682

683
    return this.workspaceName;
3✔
684
  }
685

686
  @Override
687
  public Path getWorkspacePath() {
688

689
    return this.workspacePath;
3✔
690
  }
691

692
  @Override
693
  public Path getDownloadPath() {
694

695
    return this.downloadPath;
3✔
696
  }
697

698
  @Override
699
  public Path getUrlsPath() {
700

701
    Path idePath = getIdePath();
3✔
702
    if (idePath == null) {
2!
703
      return null;
×
704
    }
705
    return idePath.resolve(FOLDER_URLS);
4✔
706
  }
707

708
  @Override
709
  public Path getToolRepositoryPath() {
710

711
    Path idePath = getIdePath();
3✔
712
    if (idePath == null) {
2!
713
      return null;
×
714
    }
715
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
716
  }
717

718
  @Override
719
  public SystemPath getPath() {
720

721
    return this.path;
3✔
722
  }
723

724
  @Override
725
  public EnvironmentVariables getVariables() {
726

727
    if (this.variables == null) {
3✔
728
      this.variables = createVariables();
4✔
729
    }
730
    return this.variables;
3✔
731
  }
732

733
  @Override
734
  public UrlMetadata getUrls() {
735

736
    if (this.urlMetadata == null) {
3✔
737
      if (!isTest()) {
3!
738
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
739
      }
740
      this.urlMetadata = new UrlMetadata(this);
6✔
741
    }
742
    return this.urlMetadata;
3✔
743
  }
744

745
  @Override
746
  public boolean isQuietMode() {
747

748
    return this.startContext.isQuietMode();
4✔
749
  }
750

751
  @Override
752
  public boolean isBatchMode() {
753

754
    return this.startContext.isBatchMode();
4✔
755
  }
756

757
  @Override
758
  public boolean isForceMode() {
759

760
    return this.startContext.isForceMode();
4✔
761
  }
762

763
  @Override
764
  public boolean isForcePull() {
765

766
    return this.startContext.isForcePull();
4✔
767
  }
768

769
  @Override
770
  public boolean isForcePlugins() {
771

772
    return this.startContext.isForcePlugins();
4✔
773
  }
774

775
  @Override
776
  public boolean isForceRepositories() {
777

778
    return this.startContext.isForceRepositories();
4✔
779
  }
780

781
  @Override
782
  public boolean isOfflineMode() {
783

784
    return this.startContext.isOfflineMode();
4✔
785
  }
786

787
  @Override
788
  public boolean isPrivacyMode() {
789
    return this.startContext.isPrivacyMode();
4✔
790
  }
791

792
  @Override
793
  public boolean isSkipUpdatesMode() {
794

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

798
  @Override
799
  public boolean isNoColorsMode() {
800

801
    return this.startContext.isNoColorsMode();
×
802
  }
803

804
  @Override
805
  public NetworkStatus getNetworkStatus() {
806

807
    if (this.networkStatus == null) {
×
808
      this.networkStatus = new NetworkStatusImpl(this);
×
809
    }
810
    return this.networkStatus;
×
811
  }
812

813
  @Override
814
  public Locale getLocale() {
815

816
    Locale locale = this.startContext.getLocale();
4✔
817
    if (locale == null) {
2✔
818
      locale = Locale.getDefault();
2✔
819
    }
820
    return locale;
2✔
821
  }
822

823
  @Override
824
  public DirectoryMerger getWorkspaceMerger() {
825

826
    if (this.workspaceMerger == null) {
3✔
827
      this.workspaceMerger = new DirectoryMerger(this);
6✔
828
    }
829
    return this.workspaceMerger;
3✔
830
  }
831

832
  /**
833
   * @return the default execution directory in which a command process is executed.
834
   */
835
  @Override
836
  public Path getDefaultExecutionDirectory() {
837

838
    return this.defaultExecutionDirectory;
×
839
  }
840

841
  /**
842
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
843
   */
844
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
845

846
    if (defaultExecutionDirectory != null) {
×
847
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
848
    }
849
  }
×
850

851
  @Override
852
  public GitContext getGitContext() {
853

854
    return new GitContextImpl(this);
×
855
  }
856

857
  @Override
858
  public ProcessContext newProcess() {
859

860
    ProcessContext processContext = createProcessContext();
3✔
861
    if (this.defaultExecutionDirectory != null) {
3!
862
      processContext.directory(this.defaultExecutionDirectory);
×
863
    }
864
    return processContext;
2✔
865
  }
866

867
  @Override
868
  public IdeSystem getSystem() {
869

870
    if (this.system == null) {
×
871
      this.system = new IdeSystemImpl();
×
872
    }
873
    return this.system;
×
874
  }
875

876
  /**
877
   * @return a new instance of {@link ProcessContext}.
878
   * @see #newProcess()
879
   */
880
  protected ProcessContext createProcessContext() {
881

882
    return new ProcessContextImpl(this);
×
883
  }
884

885
  @Override
886
  public IdeLogLevel getLogLevel() {
887

888
    return this.startContext.getLogLevel();
4✔
889
  }
890

891
  @Override
892
  public IdeLogListener getLogListener() {
893

894
    return this.startContext.getLogListener();
×
895
  }
896

897
  @Override
898
  public void logIdeHomeAndRootStatus() {
899
    if (this.ideRoot != null) {
3!
900
      IdeLogLevel.SUCCESS.log(LOG, "IDE_ROOT is set to {}", this.ideRoot);
×
901
    }
902
    if (this.ideHome == null) {
3✔
903
      LOG.warn(getMessageNotInsideIdeProject());
5✔
904
    } else {
905
      IdeLogLevel.SUCCESS.log(LOG, "IDE_HOME is set to {}", this.ideHome);
11✔
906
    }
907
  }
1✔
908

909
  @Override
910
  public String formatArgument(Object argument) {
911

912
    if (argument == null) {
2✔
913
      return null;
2✔
914
    }
915
    String result = argument.toString();
3✔
916
    if (isPrivacyMode()) {
3✔
917
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
918
        initializePrivacyMap(this.userHome, "~");
×
919
        String projectName = getProjectName();
×
920
        if (!projectName.isEmpty()) {
×
921
          this.privacyMap.put(projectName, "project");
×
922
        }
923
      }
924
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
925
        result = result.replace(entry.getKey(), entry.getValue());
×
926
      }
×
927
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
928
    }
929
    return result;
2✔
930
  }
931

932
  /**
933
   * @param path the sensitive {@link Path} to
934
   * @param replacement the replacement to mask the {@link Path} in log output.
935
   */
936
  protected void initializePrivacyMap(Path path, String replacement) {
937

938
    if (path == null) {
×
939
      return;
×
940
    }
941
    if (this.systemInfo.isWindows()) {
×
942
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
943
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
944
    } else {
945
      this.privacyMap.put(path.toString(), replacement);
×
946
    }
947
  }
×
948

949
  /**
950
   * Resets the privacy map in case fundamental values have changed.
951
   */
952
  private void resetPrivacyMap() {
953

954
    this.privacyMap.clear();
3✔
955
  }
1✔
956

957

958
  @Override
959
  public String askForInput(String message, String defaultValue) {
960

961
    while (true) {
962
      if (!message.isBlank()) {
3!
963
        IdeLogLevel.INTERACTION.log(LOG, message);
4✔
964
      }
965
      if (isBatchMode()) {
3!
966
        if (isForceMode()) {
×
967
          return defaultValue;
×
968
        } else {
969
          throw new CliAbortException();
×
970
        }
971
      }
972
      String input = readLine().trim();
4✔
973
      if (!input.isEmpty()) {
3!
974
        return input;
2✔
975
      } else {
976
        if (defaultValue != null) {
×
977
          return defaultValue;
×
978
        }
979
      }
980
    }
×
981
  }
982

983
  @Override
984
  public <O> O question(O[] options, String question, Object... args) {
985

986
    assert (options.length > 0);
4!
987
    IdeLogLevel.INTERACTION.log(LOG, question, args);
5✔
988
    return displayOptionsAndGetAnswer(options);
4✔
989
  }
990

991
  private <O> O displayOptionsAndGetAnswer(O[] options) {
992
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
993
    int i = 0;
2✔
994
    for (O option : options) {
16✔
995
      i++;
1✔
996
      String title = "" + option;
4✔
997
      String key = computeOptionKey(title);
3✔
998
      addMapping(mapping, key, option);
4✔
999
      String numericKey = Integer.toString(i);
3✔
1000
      if (numericKey.equals(key)) {
4!
1001
        LOG.trace("Options should not be numeric: {}", key);
×
1002
      } else {
1003
        addMapping(mapping, numericKey, option);
4✔
1004
      }
1005
      IdeLogLevel.INTERACTION.log(LOG, "Option {}: {}", numericKey, title);
14✔
1006
    }
1007
    O option = null;
2✔
1008
    if (isBatchMode()) {
3!
1009
      if (isForceMode()) {
×
1010
        option = options[0];
×
1011
        IdeLogLevel.INTERACTION.log(LOG, "" + option);
×
1012
      }
1013
    } else {
1014
      while (option == null) {
2✔
1015
        String answer = readLine();
3✔
1016
        option = mapping.get(answer);
4✔
1017
        if (option == null) {
2!
1018
          LOG.warn("Invalid answer: '{}' - please try again.", answer);
×
1019
        }
1020
      }
1✔
1021
    }
1022
    return option;
2✔
1023
  }
1024

1025
  private static String computeOptionKey(String option) {
1026
    String key = option;
2✔
1027
    int index = -1;
2✔
1028
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
1029
      int currentIndex = key.indexOf(c);
4✔
1030
      if (currentIndex != -1) {
3✔
1031
        if ((index == -1) || (currentIndex < index)) {
3!
1032
          index = currentIndex;
2✔
1033
        }
1034
      }
1035
    }
1036
    if (index > 0) {
2✔
1037
      key = key.substring(0, index).trim();
6✔
1038
    }
1039
    return key;
2✔
1040
  }
1041

1042
  /**
1043
   * @return the input from the end-user (e.g. read from the console).
1044
   */
1045
  protected abstract String readLine();
1046

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

1049
    O duplicate = mapping.put(key, option);
5✔
1050
    if (duplicate != null) {
2!
1051
      throw new IllegalArgumentException("Duplicated option " + key);
×
1052
    }
1053
  }
1✔
1054

1055
  @Override
1056
  public Step getCurrentStep() {
1057

1058
    return this.currentStep;
×
1059
  }
1060

1061
  @Override
1062
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1063

1064
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1065
    return this.currentStep;
3✔
1066
  }
1067

1068
  /**
1069
   * Internal method to end the running {@link Step}.
1070
   *
1071
   * @param step the current {@link Step} to end.
1072
   */
1073
  public void endStep(StepImpl step) {
1074

1075
    if (step == this.currentStep) {
4!
1076
      this.currentStep = this.currentStep.getParent();
6✔
1077
    } else {
1078
      String currentStepName = "null";
×
1079
      if (this.currentStep != null) {
×
1080
        currentStepName = this.currentStep.getName();
×
1081
      }
1082
      LOG.warn("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1083
    }
1084
  }
1✔
1085

1086
  /**
1087
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1088
   *
1089
   * @param arguments the {@link CliArgument}.
1090
   * @return the return code of the execution.
1091
   */
1092
  public int run(CliArguments arguments) {
1093

1094
    CliArgument current = arguments.current();
3✔
1095
    assert (this.currentStep == null);
4!
1096
    boolean supressStepSuccess = false;
2✔
1097
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1098
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1099
    Commandlet cmd = null;
2✔
1100
    ValidationResult result = null;
2✔
1101
    try {
1102
      while (commandletIterator.hasNext()) {
3✔
1103
        cmd = commandletIterator.next();
4✔
1104
        result = applyAndRun(arguments.copy(), cmd);
6✔
1105
        if (result.isValid()) {
3!
1106
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1107
          step.success();
2✔
1108
          return ProcessResult.SUCCESS;
4✔
1109
        }
1110
      }
1111
      activateLogging(cmd);
3✔
1112
      verifyIdeMinVersion(false);
3✔
1113
      if (result != null) {
2!
1114
        LOG.error(result.getErrorMessage());
×
1115
      }
1116
      step.error("Invalid arguments: {}", current.getArgs());
10✔
1117
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
1118
      if (cmd != null) {
2!
1119
        help.commandlet.setValue(cmd);
×
1120
      }
1121
      help.run();
2✔
1122
      return 1;
4✔
1123
    } catch (Throwable t) {
1✔
1124
      activateLogging(cmd);
3✔
1125
      step.error(t, true);
4✔
1126
      if (this.logfile != null) {
3!
1127
        System.err.println("Logfile can be found at " + this.logfile); // do not use logger
×
1128
      }
1129
      throw t;
2✔
1130
    } finally {
1131
      step.close();
2✔
1132
      assert (this.currentStep == null);
4!
1133
      step.logSummary(supressStepSuccess);
3✔
1134
    }
1135
  }
1136

1137
  /**
1138
   * Ensure the logging system is initialized.
1139
   */
1140
  private void activateLogging(Commandlet cmd) {
1141

1142
    if (!this.julConfigured) {
3!
1143
      configureJavaUtilLogging(cmd);
3✔
1144
    }
1145
    this.startContext.activateLogging();
3✔
1146
  }
1✔
1147

1148
  /**
1149
   * Configures the logging system (JUL).
1150
   *
1151
   * @param cmd the {@link Commandlet} to be called. May be {@code null}.
1152
   */
1153
  public void configureJavaUtilLogging(Commandlet cmd) {
1154

1155
    if (this.julConfigured) {
3✔
1156
      return;
1✔
1157
    }
1158
    boolean writeLogfile = isWriteLogfile(cmd);
4✔
1159
    this.startContext.setWriteLogfile(writeLogfile);
4✔
1160
    Properties properties = createJavaUtilLoggingProperties(writeLogfile, cmd);
5✔
1161
    try {
1162
      ByteArrayOutputStream out = new ByteArrayOutputStream(512);
5✔
1163
      properties.store(out, null);
4✔
1164
      out.flush();
2✔
1165
      ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
6✔
1166
      LogManager.getLogManager().readConfiguration(in);
3✔
1167
      this.julConfigured = true;
3✔
1168
      this.startContext.activateLogging();
3✔
1169
    } catch (IOException e) {
×
1170
      LOG.error("Failed to configure logging: {}", e.toString(), e);
×
1171
    }
1✔
1172
  }
1✔
1173

1174
  protected boolean isWriteLogfile(Commandlet cmd) {
1175
    if (!cmd.isWriteLogFile()) {
×
1176
      return false;
×
1177
    }
1178
    Boolean writeLogfile = IdeVariables.IDE_WRITE_LOGFILE.get(this);
×
1179
    return Boolean.TRUE.equals(writeLogfile);
×
1180
  }
1181

1182
  private Properties createJavaUtilLoggingProperties(boolean writeLogfile, Commandlet cmd) {
1183

1184
    Path idePath = getIdePath();
3✔
1185
    if (writeLogfile && (idePath == null)) {
2!
1186
      writeLogfile = false;
×
1187
      LOG.error("Cannot enable log-file since IDE_ROOT is undefined.");
×
1188
    }
1189
    Properties properties = new Properties();
4✔
1190
    String intLevel = getLogLevel().getJulLevel().getName();
5✔
1191
    String extLevel = "INFO"; // actually we want "WARNING" but there is a bug in JUL that the root level is applied to all loggers and "com.devonfw.tools.ide.level" then gets ignored.
2✔
1192
    properties.setProperty(".level", extLevel);
5✔
1193
    properties.setProperty("com.devonfw.tools.ide.level", intLevel);
5✔
1194
    if (writeLogfile) {
2!
1195
      properties.setProperty("handlers", "com.devonfw.tools.ide.log.JulConsoleHandler,java.util.logging.FileHandler");
×
1196
      properties.setProperty("java.util.logging.FileHandler.level", intLevel);
×
1197
      properties.setProperty("java.util.logging.FileHandler.formatter", "java.util.logging.SimpleFormatter");
×
1198
      properties.setProperty("java.util.logging.FileHandler.encoding", "UTF-8");
×
1199
      this.logfile = createLogfilePath(idePath, cmd);
×
1200
      getFileAccess().mkdirs(this.logfile.getParent());
×
1201
      properties.setProperty("java.util.logging.FileHandler.pattern", this.logfile.toString());
×
1202
    } else {
1203
      properties.setProperty("handlers", "com.devonfw.tools.ide.log.JulConsoleHandler");
5✔
1204
    }
1205
    properties.setProperty("com.devonfw.tools.ide.log.JulConsoleHandler.level", intLevel);
5✔
1206
    properties.setProperty("java.util.logging.SimpleFormatter.format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL [%4$s] [%3$s] %5$s%6$s%n");
5✔
1207
    return properties;
2✔
1208
  }
1209

1210
  private Path createLogfilePath(Path idePath, Commandlet cmd) {
1211
    LocalDateTime now = LocalDateTime.now();
×
1212
    Path logsPath = idePath.resolve(FOLDER_LOGS).resolve(DateTimeUtil.formatDate(now, true));
×
1213
    StringBuilder sb = new StringBuilder(32);
×
1214
    if (this.ideHome == null || ((cmd != null) && !cmd.isIdeHomeRequired())) {
×
1215
      sb.append("_ide-");
×
1216
    } else {
1217
      sb.append(this.ideHome.getFileName().toString());
×
1218
      sb.append('-');
×
1219
    }
1220
    sb.append("ide-");
×
1221
    if (cmd != null) {
×
1222
      sb.append(cmd.getName());
×
1223
      sb.append('-');
×
1224
    }
1225
    sb.append(DateTimeUtil.formatTime(now));
×
1226
    sb.append(".log");
×
1227
    return logsPath.resolve(sb.toString());
×
1228
  }
1229

1230
  @Override
1231
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1232

1233
    this.startContext.deactivateLogging(threshold);
4✔
1234
    lambda.run();
2✔
1235
    this.startContext.activateLogging();
3✔
1236
  }
1✔
1237

1238
  /**
1239
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1240
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1241
   *     {@link Commandlet} did not match and we have to try a different candidate).
1242
   */
1243
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1244

1245
    IdeLogLevel previousLogLevel = null;
2✔
1246
    cmd.reset();
2✔
1247
    ValidationResult result = apply(arguments, cmd);
5✔
1248
    if (result.isValid()) {
3!
1249
      result = cmd.validate();
3✔
1250
    }
1251
    if (result.isValid()) {
3!
1252
      LOG.debug("Running commandlet {}", cmd);
4✔
1253
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1254
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1255
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1256
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1257
      }
1258
      try {
1259
        if (cmd.isProcessableOutput()) {
3!
1260
          if (!LOG.isDebugEnabled()) {
×
1261
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1262
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
1263
          }
1264
        } else {
1265
          if (cmd.isIdeHomeRequired()) {
3!
1266
            LOG.debug(getMessageIdeHomeFound());
4✔
1267
          }
1268
          Path settingsRepository = getSettingsGitRepository();
3✔
1269
          if (settingsRepository != null) {
2!
1270
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1271
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1272
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1273
              String msg;
1274
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1275
                msg = "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.";
×
1276
              } else {
1277
                msg = "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"";
×
1278
              }
1279
              IdeLogLevel.INTERACTION.log(LOG, msg);
×
1280
            }
1281
          }
1282
        }
1283
        boolean success = ensureLicenseAgreement(cmd);
4✔
1284
        if (!success) {
2!
1285
          return ValidationResultValid.get();
×
1286
        }
1287
        cmd.run();
2✔
1288
      } finally {
1289
        this.julConfigured = false;
3✔
1290
        if (previousLogLevel != null) {
2!
1291
          this.startContext.setLogLevel(previousLogLevel);
×
1292
        }
1293
      }
1✔
1294
    } else {
1295
      LOG.trace("Commandlet did not match");
×
1296
    }
1297
    return result;
2✔
1298
  }
1299

1300
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1301

1302
    if (isTest()) {
3!
1303
      return true; // ignore for tests
2✔
1304
    }
1305
    getFileAccess().mkdirs(this.userHomeIde);
×
1306
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1307
    if (Files.isRegularFile(licenseAgreement)) {
×
1308
      return true; // success, license already accepted
×
1309
    }
1310
    if (cmd instanceof EnvironmentCommandlet) {
×
1311
      // 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
1312
      // 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
1313
      // printing anything anymore in such case.
1314
      return false;
×
1315
    }
1316
    IdeLogLevel oldLogLevel = this.startContext.getLogLevel();
×
1317
    IdeLogLevel newLogLevel = oldLogLevel;
×
1318
    if (oldLogLevel.ordinal() > IdeLogLevel.INFO.ordinal()) {
×
1319
      newLogLevel = IdeLogLevel.INFO;
×
1320
      this.startContext.setLogLevel(newLogLevel);
×
1321
    }
1322
    StringBuilder sb = new StringBuilder(1180);
×
1323
    sb.append(LOGO).append("""
×
1324
        Welcome to IDEasy!
1325
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1326
        It supports automatic download and installation of arbitrary 3rd party tools.
1327
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1328
        But if explicitly configured, also commercial software that requires an additional license may be used.
1329
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1330
        You are solely responsible for all risks implied by using this software.
1331
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1332
        You will be able to find it online under the following URL:
1333
        """).append(LICENSE_URL);
×
1334
    if (this.ideRoot != null) {
×
1335
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1336
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1337
    }
1338
    LOG.info(sb.toString());
×
1339
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1340

1341
    sb.setLength(0);
×
1342
    LocalDateTime now = LocalDateTime.now();
×
1343
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1344
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1345
    try {
1346
      Files.writeString(licenseAgreement, sb);
×
1347
    } catch (Exception e) {
×
1348
      throw new RuntimeException("Failed to save license agreement!", e);
×
1349
    }
×
1350
    if (oldLogLevel != newLogLevel) {
×
1351
      this.startContext.setLogLevel(oldLogLevel);
×
1352
    }
1353
    return true;
×
1354
  }
1355

1356
  @Override
1357
  public void verifyIdeMinVersion(boolean throwException) {
1358
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1359
    if (minVersion == null) {
2✔
1360
      return;
1✔
1361
    }
1362
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1363
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1364
          + "However, this is too old as your project requires at latest version %s\n"
1365
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1366
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1367
      if (throwException) {
2✔
1368
        throw new CliException(message);
5✔
1369
      } else {
1370
        LOG.warn(message);
3✔
1371
      }
1372
    }
1373
  }
1✔
1374

1375
  /**
1376
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1377
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1378
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1379
   */
1380
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1381

1382
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1383
    if (arguments.current().isStart()) {
4✔
1384
      arguments.next();
3✔
1385
    }
1386
    if (includeContextOptions) {
2✔
1387
      ContextCommandlet cc = new ContextCommandlet();
4✔
1388
      for (Property<?> property : cc.getProperties()) {
11✔
1389
        assert (property.isOption());
4!
1390
        property.apply(arguments, this, cc, collector);
7✔
1391
      }
1✔
1392
    }
1393
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1394
    CliArgument current = arguments.current();
3✔
1395
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1396
      collector.add(current.get(), null, null, null);
7✔
1397
    }
1398
    arguments.next();
3✔
1399
    while (commandletIterator.hasNext()) {
3✔
1400
      Commandlet cmd = commandletIterator.next();
4✔
1401
      if (!arguments.current().isEnd()) {
4✔
1402
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1403
      }
1404
    }
1✔
1405
    return collector.getSortedCandidates();
3✔
1406
  }
1407

1408
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1409

1410
    LOG.trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
5✔
1411
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1412
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1413
    List<Property<?>> properties = cmd.getProperties();
3✔
1414
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1415
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1416
    for (Property<?> property : properties) {
10✔
1417
      if (property.isOption()) {
3✔
1418
        optionProperties.add(property);
4✔
1419
      }
1420
    }
1✔
1421
    CliArgument currentArgument = arguments.current();
3✔
1422
    while (!currentArgument.isEnd()) {
3✔
1423
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1424
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1425
        if (currentArgument.isCompletion()) {
3✔
1426
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1427
          while (optionIterator.hasNext()) {
3✔
1428
            Property<?> option = optionIterator.next();
4✔
1429
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1430
            if (success) {
2✔
1431
              optionIterator.remove();
2✔
1432
              arguments.next();
3✔
1433
            }
1434
          }
1✔
1435
        } else {
1✔
1436
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1437
          if (option != null) {
2✔
1438
            arguments.next();
3✔
1439
            boolean removed = optionProperties.remove(option);
4✔
1440
            if (!removed) {
2!
1441
              option = null;
×
1442
            }
1443
          }
1444
          if (option == null) {
2✔
1445
            LOG.trace("No such option was found.");
3✔
1446
            return;
1✔
1447
          }
1448
        }
1✔
1449
      } else {
1450
        if (valueIterator.hasNext()) {
3✔
1451
          Property<?> valueProperty = valueIterator.next();
4✔
1452
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1453
          if (!success) {
2✔
1454
            LOG.trace("Completion cannot match any further.");
3✔
1455
            return;
1✔
1456
          }
1457
        } else {
1✔
1458
          LOG.trace("No value left for completion.");
3✔
1459
          return;
1✔
1460
        }
1461
      }
1462
      currentArgument = arguments.current();
4✔
1463
    }
1464
  }
1✔
1465

1466
  /**
1467
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1468
   *     {@link CliArguments#copy() copy} as needed.
1469
   * @param cmd the potential {@link Commandlet} to match.
1470
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1471
   */
1472
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1473

1474
    LOG.trace("Trying to match arguments to commandlet {}", cmd.getName());
5✔
1475
    CliArgument currentArgument = arguments.current();
3✔
1476
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1477
    Property<?> property = null;
2✔
1478
    if (propertyIterator.hasNext()) {
3!
1479
      property = propertyIterator.next();
4✔
1480
    }
1481
    while (!currentArgument.isEnd()) {
3✔
1482
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1483
      Property<?> currentProperty = property;
2✔
1484
      if (!arguments.isEndOptions()) {
3!
1485
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1486
        if (option != null) {
2!
1487
          currentProperty = option;
×
1488
        }
1489
      }
1490
      if (currentProperty == null) {
2!
1491
        LOG.trace("No option or next value found");
×
1492
        ValidationState state = new ValidationState(null);
×
1493
        state.addErrorMessage("No matching property found");
×
1494
        return state;
×
1495
      }
1496
      LOG.trace("Next property candidate to match argument is {}", currentProperty);
4✔
1497
      if (currentProperty == property) {
3!
1498
        if (!property.isMultiValued()) {
3✔
1499
          if (propertyIterator.hasNext()) {
3✔
1500
            property = propertyIterator.next();
5✔
1501
          } else {
1502
            property = null;
2✔
1503
          }
1504
        }
1505
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1506
          arguments.stopSplitShortOptions();
2✔
1507
        }
1508
      }
1509
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1510
      if (!matches) {
2!
1511
        ValidationState state = new ValidationState(null);
×
1512
        state.addErrorMessage("No matching property found");
×
1513
        return state;
×
1514
      }
1515
      currentArgument = arguments.current();
3✔
1516
    }
1✔
1517
    return ValidationResultValid.get();
2✔
1518
  }
1519

1520
  @Override
1521
  public Path findBash() {
1522
    if (this.bash != null) {
3✔
1523
      return this.bash;
3✔
1524
    }
1525
    Path bashPath = findBashOnBashPath();
3✔
1526
    if (bashPath == null) {
2✔
1527
      bashPath = findBashInPath();
3✔
1528
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1529
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1530
        if (bashPath == null) {
2!
1531
          bashPath = findBashInWindowsRegistry();
3✔
1532
        }
1533
      }
1534
    }
1535
    if (bashPath == null) {
2✔
1536
      LOG.error("No bash executable could be found on your system.");
4✔
1537
    } else {
1538
      this.bash = bashPath;
3✔
1539
    }
1540
    return bashPath;
2✔
1541
  }
1542

1543
  private Path findBashOnBashPath() {
1544
    LOG.trace("Trying to find BASH_PATH environment variable.");
3✔
1545
    Path bash;
1546
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1547
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1548
    if (bashVariable != null) {
2✔
1549
      bash = Path.of(bashVariable);
5✔
1550
      if (Files.exists(bash)) {
5✔
1551
        LOG.debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
5✔
1552
        return bash;
2✔
1553
      } else {
1554
        LOG.error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
5✔
1555
        return null;
2✔
1556
      }
1557
    } else {
1558
      LOG.debug("{} environment variable was not found", bashPathVariableName);
4✔
1559
      return null;
2✔
1560
    }
1561
  }
1562

1563
  /**
1564
   * @param path the path to check.
1565
   * @param toIgnore the String sequence which needs to be checked and ignored.
1566
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1567
   */
1568
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1569
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1570
    return !s.contains(toIgnore);
7!
1571
  }
1572

1573
  /**
1574
   * Tries to find the bash.exe within the PATH environment variable.
1575
   *
1576
   * @return Path to bash.exe if found in PATH environment variable, {@code null} if bash.exe was not found.
1577
   */
1578
  private Path findBashInPath() {
1579
    LOG.trace("Trying to find bash in PATH environment variable.");
3✔
1580
    Path bash;
1581
    String pathVariableName = IdeVariables.PATH.getName();
3✔
1582
    if (pathVariableName != null) {
2!
1583
      Path plainBash = Path.of(BASH);
5✔
1584
      Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
16!
1585
          "\\windows\\system32");
1586
      Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
6✔
1587
      bash = bashPath.toAbsolutePath();
3✔
1588
      if (bashPath.equals(plainBash)) {
4✔
1589
        LOG.warn("No usable bash executable was found in your PATH environment variable!");
3✔
1590
        bash = null;
3✔
1591
      } else {
1592
        if (Files.exists(bashPath)) {
5!
1593
          LOG.debug("A proper bash executable was found in your PATH environment variable at: {}", bash);
5✔
1594
        } else {
1595
          bash = null;
×
1596
          LOG.error("A path to a bash executable was found in your PATH environment variable at: {} but the file is not existing.", bash);
×
1597
        }
1598
      }
1599
    } else {
1✔
1600
      bash = null;
×
1601
      // this should never happen...
1602
      LOG.error("PATH environment variable was not found");
×
1603
    }
1604
    return bash;
2✔
1605
  }
1606

1607
  /**
1608
   * Tries to find the bash.exe within the Windows registry.
1609
   *
1610
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1611
   */
1612
  protected Path findBashInWindowsRegistry() {
1613
    LOG.trace("Trying to find bash in Windows registry");
×
1614
    // If not found in the default location, try the registry query
1615
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1616
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1617
    for (String bashVariant : bashVariants) {
×
1618
      LOG.trace("Trying to find bash variant: {}", bashVariant);
×
1619
      for (String registryKey : registryKeys) {
×
1620
        LOG.trace("Trying to find bash from registry key: {}", registryKey);
×
1621
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1622
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1623

1624
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1625
        if (path != null) {
×
1626
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1627
          if (Files.exists(bashPath)) {
×
1628
            LOG.debug("Found bash at: {}", bashPath);
×
1629
            return bashPath;
×
1630
          } else {
1631
            LOG.error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1632
            return null;
×
1633
          }
1634
        } else {
1635
          LOG.info("No bash executable could be found in the Windows registry.");
×
1636
        }
1637
      }
1638
    }
1639
    // no bash found
1640
    return null;
×
1641
  }
1642

1643
  private Path findBashOnWindowsDefaultGitPath() {
1644
    // Check if Git Bash exists in the default location
1645
    LOG.trace("Trying to find bash on the Windows default git path.");
3✔
1646
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1647
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1648
      LOG.trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1649
      return defaultPath;
×
1650
    }
1651
    LOG.debug("No bash was found on the Windows default git path.");
3✔
1652
    return null;
2✔
1653
  }
1654

1655
  @Override
1656
  public WindowsPathSyntax getPathSyntax() {
1657

1658
    return this.pathSyntax;
3✔
1659
  }
1660

1661
  /**
1662
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1663
   */
1664
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1665

1666
    this.pathSyntax = pathSyntax;
3✔
1667
  }
1✔
1668

1669
  /**
1670
   * @return the {@link IdeStartContextImpl}.
1671
   */
1672
  public IdeStartContextImpl getStartContext() {
1673

1674
    return startContext;
3✔
1675
  }
1676

1677
  /**
1678
   * @return the {@link WindowsHelper}.
1679
   */
1680
  public final WindowsHelper getWindowsHelper() {
1681

1682
    if (this.windowsHelper == null) {
3✔
1683
      this.windowsHelper = createWindowsHelper();
4✔
1684
    }
1685
    return this.windowsHelper;
3✔
1686
  }
1687

1688
  /**
1689
   * @return the new {@link WindowsHelper} instance.
1690
   */
1691
  protected WindowsHelper createWindowsHelper() {
1692

1693
    return new WindowsHelperImpl(this);
×
1694
  }
1695

1696
  /**
1697
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1698
   */
1699
  public void reload() {
1700

1701
    this.variables = null;
3✔
1702
    this.customToolRepository = null;
3✔
1703
  }
1✔
1704

1705
  @Override
1706
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1707

1708
    assert (Files.isDirectory(installationPath));
6!
1709
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1710
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1711
  }
1✔
1712

1713
  /*
1714
   * @param home the IDE_HOME directory.
1715
   * @param workspace the name of the active workspace folder.
1716
   */
1717
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1718

1719
  }
1720

1721
  /**
1722
   * Returns the default git path on Windows. Required to be overwritten in tests.
1723
   *
1724
   * @return default path to git on Windows.
1725
   */
1726
  public String getDefaultWindowsGitPath() {
1727
    return DEFAULT_WINDOWS_GIT_PATH;
×
1728
  }
1729

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