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

devonfw / IDEasy / 22284264868

22 Feb 2026 08:00PM UTC coverage: 70.75% (+0.3%) from 70.474%
22284264868

Pull #1714

github

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

4063 of 6346 branches covered (64.02%)

Branch coverage included in aggregate %.

10636 of 14430 relevant lines covered (73.71%)

3.1 hits per line

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

67.42
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
  /**
172
   * The constructor.
173
   *
174
   * @param startContext the {@link IdeStartContextImpl}.
175
   * @param workingDirectory the optional {@link Path} to current working directory.
176
   */
177
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
178

179
    super();
2✔
180

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

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

213
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
214

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

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

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

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

261
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
262
  }
263

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

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

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

285
  private Path findIdeRoot(Path ideHomePath) {
286

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

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

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

341
  @Override
342
  public void setCwd(Path userDir, String workspace, Path ideHome) {
343

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

373
  private String getMessageIdeHomeFound() {
374

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

382
  private String getMessageNotInsideIdeProject() {
383

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

387
  private String getMessageIdeRootNotFound() {
388

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

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

403
    return false;
×
404
  }
405

406
  protected SystemPath computeSystemPath() {
407

408
    return new SystemPath(this);
×
409
  }
410

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

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

427
  private EnvironmentVariables createVariables() {
428

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

437
  protected AbstractEnvironmentVariables createSystemVariables() {
438

439
    return EnvironmentVariables.ofSystem(this);
3✔
440
  }
441

442
  @Override
443
  public SystemInfo getSystemInfo() {
444

445
    return this.systemInfo;
3✔
446
  }
447

448
  @Override
449
  public FileAccess getFileAccess() {
450

451
    return this.fileAccess;
3✔
452
  }
453

454
  @Override
455
  public CommandletManager getCommandletManager() {
456

457
    return this.commandletManager;
3✔
458
  }
459

460
  @Override
461
  public ToolRepository getDefaultToolRepository() {
462

463
    return this.defaultToolRepository;
3✔
464
  }
465

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

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

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

490
  @Override
491
  public CustomToolRepository getCustomToolRepository() {
492

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

499
  @Override
500
  public Path getIdeHome() {
501

502
    return this.ideHome;
3✔
503
  }
504

505
  @Override
506
  public String getProjectName() {
507

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

514
  @Override
515
  public VersionIdentifier getProjectVersion() {
516

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

527
  @Override
528
  public void setProjectVersion(VersionIdentifier version) {
529

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

538
  @Override
539
  public Path getIdeRoot() {
540

541
    return this.ideRoot;
3✔
542
  }
543

544
  @Override
545
  public Path getIdePath() {
546

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

554
  @Override
555
  public Path getCwd() {
556

557
    return this.cwd;
3✔
558
  }
559

560
  @Override
561
  public Path getTempPath() {
562

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

570
  @Override
571
  public Path getTempDownloadPath() {
572

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

580
  @Override
581
  public Path getUserHome() {
582

583
    return this.userHome;
3✔
584
  }
585

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

593
    this.userHome = userHome;
3✔
594
    resetPrivacyMap();
2✔
595
  }
1✔
596

597
  @Override
598
  public Path getUserHomeIde() {
599

600
    return this.userHomeIde;
3✔
601
  }
602

603
  @Override
604
  public Path getSettingsPath() {
605

606
    return this.settingsPath;
3✔
607
  }
608

609
  @Override
610
  public Path getSettingsGitRepository() {
611

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

621
  @Override
622
  public boolean isSettingsRepositorySymlinkOrJunction() {
623

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

631
  @Override
632
  public Path getSettingsCommitIdPath() {
633

634
    return this.settingsCommitIdPath;
3✔
635
  }
636

637
  @Override
638
  public Path getConfPath() {
639

640
    return this.confPath;
3✔
641
  }
642

643
  @Override
644
  public Path getSoftwarePath() {
645

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

652
  @Override
653
  public Path getSoftwareExtraPath() {
654

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

662
  @Override
663
  public Path getSoftwareRepositoryPath() {
664

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

672
  @Override
673
  public Path getPluginsPath() {
674

675
    return this.pluginsPath;
3✔
676
  }
677

678
  @Override
679
  public String getWorkspaceName() {
680

681
    return this.workspaceName;
3✔
682
  }
683

684
  @Override
685
  public Path getWorkspacePath() {
686

687
    return this.workspacePath;
3✔
688
  }
689

690
  @Override
691
  public Path getDownloadPath() {
692

693
    return this.downloadPath;
3✔
694
  }
695

696
  @Override
697
  public Path getUrlsPath() {
698

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

706
  @Override
707
  public Path getToolRepositoryPath() {
708

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

716
  @Override
717
  public SystemPath getPath() {
718

719
    return this.path;
3✔
720
  }
721

722
  @Override
723
  public EnvironmentVariables getVariables() {
724

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

731
  @Override
732
  public UrlMetadata getUrls() {
733

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

743
  @Override
744
  public boolean isQuietMode() {
745

746
    return this.startContext.isQuietMode();
4✔
747
  }
748

749
  @Override
750
  public boolean isBatchMode() {
751

752
    return this.startContext.isBatchMode();
4✔
753
  }
754

755
  @Override
756
  public boolean isForceMode() {
757

758
    return this.startContext.isForceMode();
4✔
759
  }
760

761
  @Override
762
  public boolean isForcePull() {
763

764
    return this.startContext.isForcePull();
4✔
765
  }
766

767
  @Override
768
  public boolean isForcePlugins() {
769

770
    return this.startContext.isForcePlugins();
4✔
771
  }
772

773
  @Override
774
  public boolean isForceRepositories() {
775

776
    return this.startContext.isForceRepositories();
4✔
777
  }
778

779
  @Override
780
  public boolean isOfflineMode() {
781

782
    return this.startContext.isOfflineMode();
4✔
783
  }
784

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

790
  @Override
791
  public boolean isSkipUpdatesMode() {
792

793
    return this.startContext.isSkipUpdatesMode();
4✔
794
  }
795

796
  @Override
797
  public boolean isNoColorsMode() {
798

799
    return this.startContext.isNoColorsMode();
×
800
  }
801

802
  @Override
803
  public NetworkStatus getNetworkStatus() {
804

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

811
  @Override
812
  public Locale getLocale() {
813

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

821
  @Override
822
  public DirectoryMerger getWorkspaceMerger() {
823

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

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

836
    return this.defaultExecutionDirectory;
×
837
  }
838

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

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

849
  @Override
850
  public GitContext getGitContext() {
851

852
    return new GitContextImpl(this);
×
853
  }
854

855
  @Override
856
  public ProcessContext newProcess() {
857

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

865
  @Override
866
  public IdeSystem getSystem() {
867

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

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

880
    return new ProcessContextImpl(this);
×
881
  }
882

883
  @Override
884
  public IdeLogLevel getLogLevel() {
885

886
    return this.startContext.getLogLevel();
4✔
887
  }
888

889
  @Override
890
  public IdeLogListener getLogListener() {
891

892
    return this.startContext.getLogListener();
×
893
  }
894

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

907
  @Override
908
  public String formatArgument(Object argument) {
909

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

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

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

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

952
    this.privacyMap.clear();
3✔
953
  }
1✔
954

955

956
  @Override
957
  public String askForInput(String message, String defaultValue) {
958

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

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

984
    assert (options.length > 0);
4!
985
    LOG.info(IdeLogLevel.INTERACTION.getSlf4jMarker(), question, args);
6✔
986
    return displayOptionsAndGetAnswer(options);
4✔
987
  }
988

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

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

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

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

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

1053
  @Override
1054
  public Step getCurrentStep() {
1055

1056
    return this.currentStep;
×
1057
  }
1058

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

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

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

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

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

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

1132
  /**
1133
   * Configures the logging system (JUL).
1134
   *
1135
   * @param logfile value of {@link Commandlet#isWriteLogFile()}.
1136
   */
1137
  public void configureJavaUtilLogging(boolean logfile) {
1138

1139
    if (this.julConfigured) {
3✔
1140
      return;
1✔
1141
    }
1142
    Properties properties = createJavaUtilLoggingProperties(logfile);
4✔
1143
    try {
1144
      ByteArrayOutputStream out = new ByteArrayOutputStream(512);
5✔
1145
      properties.store(out, null);
4✔
1146
      out.flush();
2✔
1147
      ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
6✔
1148
      LogManager.getLogManager().readConfiguration(in);
3✔
1149
      this.julConfigured = true;
3✔
1150
      this.startContext.activateLogging();
3✔
1151
    } catch (IOException e) {
×
1152
      LOG.error("Failed to configure logging: {}", e.toString(), e);
×
1153
    }
1✔
1154
  }
1✔
1155

1156
  protected Properties createJavaUtilLoggingProperties(boolean logfile) {
1157

1158
    if (logfile) {
×
1159
      Boolean writeLogfile = IdeVariables.IDE_WRITE_LOGFILE.get(this);
×
1160
      if (Boolean.FALSE.equals(writeLogfile)) {
×
1161
        logfile = false;
×
1162
      }
1163
    }
1164
    this.startContext.setWriteLogfile(logfile);
×
1165
    return doCreateJavaUtilLoggingProperties(logfile);
×
1166
  }
1167

1168
  protected final Properties doCreateJavaUtilLoggingProperties(boolean file) {
1169

1170
    Path idePath = getIdePath();
3✔
1171
    if (file && (idePath == null)) {
2!
1172
      file = false;
×
1173
      LOG.error("Cannot enable log-file since IDE_ROOT is undefined.");
×
1174
    }
1175
    Properties properties = new Properties();
4✔
1176
    String intLevel = getLogLevel().getJulLevel().getName();
5✔
1177
    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✔
1178
    properties.setProperty(".level", extLevel);
5✔
1179
    properties.setProperty("com.devonfw.tools.ide.level", intLevel);
5✔
1180
    if (file) {
2!
1181
      properties.setProperty("handlers", "com.devonfw.tools.ide.log.JulConsoleHandler,java.util.logging.FileHandler");
×
1182
      properties.setProperty("java.util.logging.FileHandler.level", intLevel);
×
1183
      properties.setProperty("java.util.logging.FileHandler.formatter", "java.util.logging.SimpleFormatter");
×
1184
      properties.setProperty("java.util.logging.FileHandler.encoding", "UTF-8");
×
1185
      LocalDateTime now = LocalDateTime.now();
×
1186
      Path logsPath = idePath.resolve(FOLDER_LOGS).resolve(DateTimeUtil.formatDate(now, true));
×
1187
      getFileAccess().mkdirs(logsPath);
×
1188
      properties.setProperty("java.util.logging.FileHandler.pattern", logsPath.resolve("ideasy-" + DateTimeUtil.formatTime(now) + ".log").toString());
×
1189
    } else {
×
1190
      properties.setProperty("handlers", "com.devonfw.tools.ide.log.JulConsoleHandler");
5✔
1191
    }
1192
    properties.setProperty("com.devonfw.tools.ide.log.JulConsoleHandler.level", intLevel);
5✔
1193
    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✔
1194
    return properties;
2✔
1195
  }
1196

1197
  @Override
1198
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1199

1200
    this.startContext.deactivateLogging(threshold);
4✔
1201
    lambda.run();
2✔
1202
    this.startContext.activateLogging();
3✔
1203
  }
1✔
1204

1205
  /**
1206
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1207
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1208
   *     {@link Commandlet} did not match and we have to try a different candidate).
1209
   */
1210
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1211

1212
    IdeLogLevel previousLogLevel = null;
2✔
1213
    cmd.reset();
2✔
1214
    ValidationResult result = apply(arguments, cmd);
5✔
1215
    if (result.isValid()) {
3!
1216
      result = cmd.validate();
3✔
1217
    }
1218
    if (result.isValid()) {
3!
1219
      LOG.debug("Running commandlet {}", cmd);
4✔
1220
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1221
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1222
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1223
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1224
      }
1225
      try {
1226
        if (cmd.isProcessableOutput()) {
3!
1227
          if (!LOG.isDebugEnabled()) {
×
1228
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1229
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
1230
          }
1231
        } else {
1232
          if (cmd.isIdeHomeRequired()) {
3!
1233
            LOG.debug(getMessageIdeHomeFound());
4✔
1234
          }
1235
          Path settingsRepository = getSettingsGitRepository();
3✔
1236
          if (settingsRepository != null) {
2!
1237
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1238
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1239
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1240
              String msg;
1241
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1242
                msg = "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.";
×
1243
              } else {
1244
                msg = "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"";
×
1245
              }
1246
              LOG.info(IdeLogLevel.INTERACTION.getSlf4jMarker(), msg);
×
1247
            }
1248
          }
1249
        }
1250
        boolean success = ensureLicenseAgreement(cmd);
4✔
1251
        if (!success) {
2!
1252
          return ValidationResultValid.get();
×
1253
        }
1254
        cmd.run();
2✔
1255
      } finally {
1256
        this.julConfigured = false;
3✔
1257
        if (previousLogLevel != null) {
2!
1258
          this.startContext.setLogLevel(previousLogLevel);
×
1259
        }
1260
      }
1✔
1261
    } else {
1262
      LOG.trace("Commandlet did not match");
×
1263
    }
1264
    return result;
2✔
1265
  }
1266

1267
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1268

1269
    if (isTest()) {
3!
1270
      return true; // ignore for tests
2✔
1271
    }
1272
    getFileAccess().mkdirs(this.userHomeIde);
×
1273
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1274
    if (Files.isRegularFile(licenseAgreement)) {
×
1275
      return true; // success, license already accepted
×
1276
    }
1277
    if (cmd instanceof EnvironmentCommandlet) {
×
1278
      // 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
1279
      // 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
1280
      // printing anything anymore in such case.
1281
      return false;
×
1282
    }
1283
    IdeLogLevel oldLogLevel = this.startContext.getLogLevel();
×
1284
    IdeLogLevel newLogLevel = oldLogLevel;
×
1285
    if (oldLogLevel.ordinal() > IdeLogLevel.INFO.ordinal()) {
×
1286
      newLogLevel = IdeLogLevel.INFO;
×
1287
      this.startContext.setLogLevel(newLogLevel);
×
1288
    }
1289
    StringBuilder sb = new StringBuilder(1180);
×
1290
    sb.append(LOGO).append("""
×
1291
        Welcome to IDEasy!
1292
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1293
        It supports automatic download and installation of arbitrary 3rd party tools.
1294
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1295
        But if explicitly configured, also commercial software that requires an additional license may be used.
1296
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1297
        You are solely responsible for all risks implied by using this software.
1298
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1299
        You will be able to find it online under the following URL:
1300
        """).append(LICENSE_URL);
×
1301
    if (this.ideRoot != null) {
×
1302
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1303
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1304
    }
1305
    LOG.info(sb.toString());
×
1306
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1307

1308
    sb.setLength(0);
×
1309
    LocalDateTime now = LocalDateTime.now();
×
1310
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1311
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1312
    try {
1313
      Files.writeString(licenseAgreement, sb);
×
1314
    } catch (Exception e) {
×
1315
      throw new RuntimeException("Failed to save license agreement!", e);
×
1316
    }
×
1317
    if (oldLogLevel != newLogLevel) {
×
1318
      this.startContext.setLogLevel(oldLogLevel);
×
1319
    }
1320
    return true;
×
1321
  }
1322

1323
  @Override
1324
  public void verifyIdeMinVersion(boolean throwException) {
1325
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1326
    if (minVersion == null) {
2✔
1327
      return;
1✔
1328
    }
1329
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1330
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1331
          + "However, this is too old as your project requires at latest version %s\n"
1332
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1333
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1334
      if (throwException) {
2✔
1335
        throw new CliException(message);
5✔
1336
      } else {
1337
        LOG.warn(message);
3✔
1338
      }
1339
    }
1340
  }
1✔
1341

1342
  /**
1343
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1344
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1345
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1346
   */
1347
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1348

1349
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1350
    if (arguments.current().isStart()) {
4✔
1351
      arguments.next();
3✔
1352
    }
1353
    if (includeContextOptions) {
2✔
1354
      ContextCommandlet cc = new ContextCommandlet();
4✔
1355
      for (Property<?> property : cc.getProperties()) {
11✔
1356
        assert (property.isOption());
4!
1357
        property.apply(arguments, this, cc, collector);
7✔
1358
      }
1✔
1359
    }
1360
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1361
    CliArgument current = arguments.current();
3✔
1362
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1363
      collector.add(current.get(), null, null, null);
7✔
1364
    }
1365
    arguments.next();
3✔
1366
    while (commandletIterator.hasNext()) {
3✔
1367
      Commandlet cmd = commandletIterator.next();
4✔
1368
      if (!arguments.current().isEnd()) {
4✔
1369
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1370
      }
1371
    }
1✔
1372
    return collector.getSortedCandidates();
3✔
1373
  }
1374

1375
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1376

1377
    LOG.trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
5✔
1378
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1379
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1380
    List<Property<?>> properties = cmd.getProperties();
3✔
1381
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1382
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1383
    for (Property<?> property : properties) {
10✔
1384
      if (property.isOption()) {
3✔
1385
        optionProperties.add(property);
4✔
1386
      }
1387
    }
1✔
1388
    CliArgument currentArgument = arguments.current();
3✔
1389
    while (!currentArgument.isEnd()) {
3✔
1390
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1391
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1392
        if (currentArgument.isCompletion()) {
3✔
1393
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1394
          while (optionIterator.hasNext()) {
3✔
1395
            Property<?> option = optionIterator.next();
4✔
1396
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1397
            if (success) {
2✔
1398
              optionIterator.remove();
2✔
1399
              arguments.next();
3✔
1400
            }
1401
          }
1✔
1402
        } else {
1✔
1403
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1404
          if (option != null) {
2✔
1405
            arguments.next();
3✔
1406
            boolean removed = optionProperties.remove(option);
4✔
1407
            if (!removed) {
2!
1408
              option = null;
×
1409
            }
1410
          }
1411
          if (option == null) {
2✔
1412
            LOG.trace("No such option was found.");
3✔
1413
            return;
1✔
1414
          }
1415
        }
1✔
1416
      } else {
1417
        if (valueIterator.hasNext()) {
3✔
1418
          Property<?> valueProperty = valueIterator.next();
4✔
1419
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1420
          if (!success) {
2✔
1421
            LOG.trace("Completion cannot match any further.");
3✔
1422
            return;
1✔
1423
          }
1424
        } else {
1✔
1425
          LOG.trace("No value left for completion.");
3✔
1426
          return;
1✔
1427
        }
1428
      }
1429
      currentArgument = arguments.current();
4✔
1430
    }
1431
  }
1✔
1432

1433
  /**
1434
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1435
   *     {@link CliArguments#copy() copy} as needed.
1436
   * @param cmd the potential {@link Commandlet} to match.
1437
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1438
   */
1439
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1440

1441
    LOG.trace("Trying to match arguments to commandlet {}", cmd.getName());
5✔
1442
    CliArgument currentArgument = arguments.current();
3✔
1443
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1444
    Property<?> property = null;
2✔
1445
    if (propertyIterator.hasNext()) {
3!
1446
      property = propertyIterator.next();
4✔
1447
    }
1448
    while (!currentArgument.isEnd()) {
3✔
1449
      LOG.trace("Trying to match argument '{}'", currentArgument);
4✔
1450
      Property<?> currentProperty = property;
2✔
1451
      if (!arguments.isEndOptions()) {
3!
1452
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1453
        if (option != null) {
2!
1454
          currentProperty = option;
×
1455
        }
1456
      }
1457
      if (currentProperty == null) {
2!
1458
        LOG.trace("No option or next value found");
×
1459
        ValidationState state = new ValidationState(null);
×
1460
        state.addErrorMessage("No matching property found");
×
1461
        return state;
×
1462
      }
1463
      LOG.trace("Next property candidate to match argument is {}", currentProperty);
4✔
1464
      if (currentProperty == property) {
3!
1465
        if (!property.isMultiValued()) {
3✔
1466
          if (propertyIterator.hasNext()) {
3✔
1467
            property = propertyIterator.next();
5✔
1468
          } else {
1469
            property = null;
2✔
1470
          }
1471
        }
1472
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1473
          arguments.stopSplitShortOptions();
2✔
1474
        }
1475
      }
1476
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1477
      if (!matches) {
2!
1478
        ValidationState state = new ValidationState(null);
×
1479
        state.addErrorMessage("No matching property found");
×
1480
        return state;
×
1481
      }
1482
      currentArgument = arguments.current();
3✔
1483
    }
1✔
1484
    return ValidationResultValid.get();
2✔
1485
  }
1486

1487
  @Override
1488
  public Path findBash() {
1489
    if (this.bash != null) {
3✔
1490
      return this.bash;
3✔
1491
    }
1492
    Path bashPath = findBashOnBashPath();
3✔
1493
    if (bashPath == null) {
2✔
1494
      bashPath = findBashInPath();
3✔
1495
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1496
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1497
        if (bashPath == null) {
2!
1498
          bashPath = findBashInWindowsRegistry();
3✔
1499
        }
1500
      }
1501
    }
1502
    if (bashPath == null) {
2✔
1503
      LOG.error("No bash executable could be found on your system.");
4✔
1504
    } else {
1505
      this.bash = bashPath;
3✔
1506
    }
1507
    return bashPath;
2✔
1508
  }
1509

1510
  private Path findBashOnBashPath() {
1511
    LOG.trace("Trying to find BASH_PATH environment variable.");
3✔
1512
    Path bash;
1513
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1514
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1515
    if (bashVariable != null) {
2✔
1516
      bash = Path.of(bashVariable);
5✔
1517
      if (Files.exists(bash)) {
5✔
1518
        LOG.debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
5✔
1519
        return bash;
2✔
1520
      } else {
1521
        LOG.error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
5✔
1522
        return null;
2✔
1523
      }
1524
    } else {
1525
      LOG.debug("{} environment variable was not found", bashPathVariableName);
4✔
1526
      return null;
2✔
1527
    }
1528
  }
1529

1530
  /**
1531
   * @param path the path to check.
1532
   * @param toIgnore the String sequence which needs to be checked and ignored.
1533
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1534
   */
1535
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1536
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1537
    return !s.contains(toIgnore);
7!
1538
  }
1539

1540
  /**
1541
   * Tries to find the bash.exe within the PATH environment variable.
1542
   *
1543
   * @return Path to bash.exe if found in PATH environment variable, {@code null} if bash.exe was not found.
1544
   */
1545
  private Path findBashInPath() {
1546
    LOG.trace("Trying to find bash in PATH environment variable.");
3✔
1547
    Path bash;
1548
    String pathVariableName = IdeVariables.PATH.getName();
3✔
1549
    if (pathVariableName != null) {
2!
1550
      Path plainBash = Path.of(BASH);
5✔
1551
      Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
16!
1552
          "\\windows\\system32");
1553
      Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
6✔
1554
      bash = bashPath.toAbsolutePath();
3✔
1555
      if (bashPath.equals(plainBash)) {
4✔
1556
        LOG.warn("No usable bash executable was found in your PATH environment variable!");
3✔
1557
        bash = null;
3✔
1558
      } else {
1559
        if (Files.exists(bashPath)) {
5!
1560
          LOG.debug("A proper bash executable was found in your PATH environment variable at: {}", bash);
5✔
1561
        } else {
1562
          bash = null;
×
1563
          LOG.error("A path to a bash executable was found in your PATH environment variable at: {} but the file is not existing.", bash);
×
1564
        }
1565
      }
1566
    } else {
1✔
1567
      bash = null;
×
1568
      // this should never happen...
1569
      LOG.error("PATH environment variable was not found");
×
1570
    }
1571
    return bash;
2✔
1572
  }
1573

1574
  /**
1575
   * Tries to find the bash.exe within the Windows registry.
1576
   *
1577
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1578
   */
1579
  protected Path findBashInWindowsRegistry() {
1580
    LOG.trace("Trying to find bash in Windows registry");
×
1581
    // If not found in the default location, try the registry query
1582
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1583
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1584
    for (String bashVariant : bashVariants) {
×
1585
      LOG.trace("Trying to find bash variant: {}", bashVariant);
×
1586
      for (String registryKey : registryKeys) {
×
1587
        LOG.trace("Trying to find bash from registry key: {}", registryKey);
×
1588
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1589
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1590

1591
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1592
        if (path != null) {
×
1593
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1594
          if (Files.exists(bashPath)) {
×
1595
            LOG.debug("Found bash at: {}", bashPath);
×
1596
            return bashPath;
×
1597
          } else {
1598
            LOG.error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1599
            return null;
×
1600
          }
1601
        } else {
1602
          LOG.info("No bash executable could be found in the Windows registry.");
×
1603
        }
1604
      }
1605
    }
1606
    // no bash found
1607
    return null;
×
1608
  }
1609

1610
  private Path findBashOnWindowsDefaultGitPath() {
1611
    // Check if Git Bash exists in the default location
1612
    LOG.trace("Trying to find bash on the Windows default git path.");
3✔
1613
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1614
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1615
      LOG.trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1616
      return defaultPath;
×
1617
    }
1618
    LOG.debug("No bash was found on the Windows default git path.");
3✔
1619
    return null;
2✔
1620
  }
1621

1622
  @Override
1623
  public WindowsPathSyntax getPathSyntax() {
1624

1625
    return this.pathSyntax;
3✔
1626
  }
1627

1628
  /**
1629
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1630
   */
1631
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1632

1633
    this.pathSyntax = pathSyntax;
3✔
1634
  }
1✔
1635

1636
  /**
1637
   * @return the {@link IdeStartContextImpl}.
1638
   */
1639
  public IdeStartContextImpl getStartContext() {
1640

1641
    return startContext;
3✔
1642
  }
1643

1644
  /**
1645
   * @return the {@link WindowsHelper}.
1646
   */
1647
  public final WindowsHelper getWindowsHelper() {
1648

1649
    if (this.windowsHelper == null) {
3✔
1650
      this.windowsHelper = createWindowsHelper();
4✔
1651
    }
1652
    return this.windowsHelper;
3✔
1653
  }
1654

1655
  /**
1656
   * @return the new {@link WindowsHelper} instance.
1657
   */
1658
  protected WindowsHelper createWindowsHelper() {
1659

1660
    return new WindowsHelperImpl(this);
×
1661
  }
1662

1663
  /**
1664
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1665
   */
1666
  public void reload() {
1667

1668
    this.variables = null;
3✔
1669
    this.customToolRepository = null;
3✔
1670
  }
1✔
1671

1672
  @Override
1673
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1674

1675
    assert (Files.isDirectory(installationPath));
6!
1676
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1677
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1678
  }
1✔
1679

1680
  /*
1681
   * @param home the IDE_HOME directory.
1682
   * @param workspace the name of the active workspace folder.
1683
   */
1684
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1685

1686
  }
1687

1688
  /**
1689
   * Returns the default git path on Windows. Required to be overwritten in tests.
1690
   *
1691
   * @return default path to git on Windows.
1692
   */
1693
  public String getDefaultWindowsGitPath() {
1694
    return DEFAULT_WINDOWS_GIT_PATH;
×
1695
  }
1696

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