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

devonfw / IDEasy / 22317391561

23 Feb 2026 05:30PM UTC coverage: 70.257% (-0.2%) from 70.474%
22317391561

Pull #1714

github

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

4066 of 6384 branches covered (63.69%)

Branch coverage included in aggregate %.

10598 of 14488 relevant lines covered (73.15%)

3.08 hits per line

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

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

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

217
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
218

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

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

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

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

265
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
266
  }
267

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

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

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

289
  private Path findIdeRoot(Path ideHomePath) {
290

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

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

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

345
  @Override
346
  public void setCwd(Path userDir, String workspace, Path ideHome) {
347

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

377
  private String getMessageIdeHomeFound() {
378

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

386
  private String getMessageNotInsideIdeProject() {
387

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

391
  private String getMessageIdeRootNotFound() {
392

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

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

407
    return false;
×
408
  }
409

410
  protected SystemPath computeSystemPath() {
411

412
    return new SystemPath(this);
×
413
  }
414

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

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

431
  private EnvironmentVariables createVariables() {
432

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

441
  protected AbstractEnvironmentVariables createSystemVariables() {
442

443
    return EnvironmentVariables.ofSystem(this);
3✔
444
  }
445

446
  @Override
447
  public SystemInfo getSystemInfo() {
448

449
    return this.systemInfo;
3✔
450
  }
451

452
  @Override
453
  public FileAccess getFileAccess() {
454

455
    return this.fileAccess;
3✔
456
  }
457

458
  @Override
459
  public CommandletManager getCommandletManager() {
460

461
    return this.commandletManager;
3✔
462
  }
463

464
  @Override
465
  public ToolRepository getDefaultToolRepository() {
466

467
    return this.defaultToolRepository;
3✔
468
  }
469

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

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

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

494
  @Override
495
  public CustomToolRepository getCustomToolRepository() {
496

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

503
  @Override
504
  public Path getIdeHome() {
505

506
    return this.ideHome;
3✔
507
  }
508

509
  @Override
510
  public String getProjectName() {
511

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

518
  @Override
519
  public VersionIdentifier getProjectVersion() {
520

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

531
  @Override
532
  public void setProjectVersion(VersionIdentifier version) {
533

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

542
  @Override
543
  public Path getIdeRoot() {
544

545
    return this.ideRoot;
3✔
546
  }
547

548
  @Override
549
  public Path getIdePath() {
550

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

558
  @Override
559
  public Path getCwd() {
560

561
    return this.cwd;
3✔
562
  }
563

564
  @Override
565
  public Path getTempPath() {
566

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

574
  @Override
575
  public Path getTempDownloadPath() {
576

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

584
  @Override
585
  public Path getUserHome() {
586

587
    return this.userHome;
3✔
588
  }
589

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

597
    this.userHome = userHome;
3✔
598
    resetPrivacyMap();
2✔
599
  }
1✔
600

601
  @Override
602
  public Path getUserHomeIde() {
603

604
    return this.userHomeIde;
3✔
605
  }
606

607
  @Override
608
  public Path getSettingsPath() {
609

610
    return this.settingsPath;
3✔
611
  }
612

613
  @Override
614
  public Path getSettingsGitRepository() {
615

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

625
  @Override
626
  public boolean isSettingsRepositorySymlinkOrJunction() {
627

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

635
  @Override
636
  public Path getSettingsCommitIdPath() {
637

638
    return this.settingsCommitIdPath;
3✔
639
  }
640

641
  @Override
642
  public Path getConfPath() {
643

644
    return this.confPath;
3✔
645
  }
646

647
  @Override
648
  public Path getSoftwarePath() {
649

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

656
  @Override
657
  public Path getSoftwareExtraPath() {
658

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

666
  @Override
667
  public Path getSoftwareRepositoryPath() {
668

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

676
  @Override
677
  public Path getPluginsPath() {
678

679
    return this.pluginsPath;
3✔
680
  }
681

682
  @Override
683
  public String getWorkspaceName() {
684

685
    return this.workspaceName;
3✔
686
  }
687

688
  @Override
689
  public Path getWorkspacePath() {
690

691
    return this.workspacePath;
3✔
692
  }
693

694
  @Override
695
  public Path getDownloadPath() {
696

697
    return this.downloadPath;
3✔
698
  }
699

700
  @Override
701
  public Path getUrlsPath() {
702

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

710
  @Override
711
  public Path getToolRepositoryPath() {
712

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

720
  @Override
721
  public SystemPath getPath() {
722

723
    return this.path;
3✔
724
  }
725

726
  @Override
727
  public EnvironmentVariables getVariables() {
728

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

735
  @Override
736
  public UrlMetadata getUrls() {
737

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

747
  @Override
748
  public boolean isQuietMode() {
749

750
    return this.startContext.isQuietMode();
4✔
751
  }
752

753
  @Override
754
  public boolean isBatchMode() {
755

756
    return this.startContext.isBatchMode();
4✔
757
  }
758

759
  @Override
760
  public boolean isForceMode() {
761

762
    return this.startContext.isForceMode();
4✔
763
  }
764

765
  @Override
766
  public boolean isForcePull() {
767

768
    return this.startContext.isForcePull();
4✔
769
  }
770

771
  @Override
772
  public boolean isForcePlugins() {
773

774
    return this.startContext.isForcePlugins();
4✔
775
  }
776

777
  @Override
778
  public boolean isForceRepositories() {
779

780
    return this.startContext.isForceRepositories();
4✔
781
  }
782

783
  @Override
784
  public boolean isOfflineMode() {
785

786
    return this.startContext.isOfflineMode();
4✔
787
  }
788

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

794
  @Override
795
  public boolean isSkipUpdatesMode() {
796

797
    return this.startContext.isSkipUpdatesMode();
4✔
798
  }
799

800
  @Override
801
  public boolean isNoColorsMode() {
802

803
    return this.startContext.isNoColorsMode();
×
804
  }
805

806
  @Override
807
  public NetworkStatus getNetworkStatus() {
808

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

815
  @Override
816
  public Locale getLocale() {
817

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

825
  @Override
826
  public DirectoryMerger getWorkspaceMerger() {
827

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

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

840
    return this.defaultExecutionDirectory;
×
841
  }
842

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

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

853
  @Override
854
  public GitContext getGitContext() {
855

856
    return new GitContextImpl(this);
×
857
  }
858

859
  @Override
860
  public ProcessContext newProcess() {
861

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

869
  @Override
870
  public IdeSystem getSystem() {
871

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

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

884
    return new ProcessContextImpl(this);
×
885
  }
886

887
  @Override
888
  public IdeLogLevel getLogLevelConsole() {
889

890
    return this.startContext.getLogLevelConsole();
×
891
  }
892

893
  @Override
894
  public IdeLogLevel getLogLevelLogger() {
895

896
    return this.startContext.getLogLevelLogger();
×
897
  }
898

899
  @Override
900
  public IdeLogListener getLogListener() {
901

902
    return this.startContext.getLogListener();
×
903
  }
904

905
  @Override
906
  public void logIdeHomeAndRootStatus() {
907
    if (this.ideRoot != null) {
3!
908
      IdeLogLevel.SUCCESS.log(LOG, "IDE_ROOT is set to {}", this.ideRoot);
×
909
    }
910
    if (this.ideHome == null) {
3✔
911
      LOG.warn(getMessageNotInsideIdeProject());
5✔
912
    } else {
913
      IdeLogLevel.SUCCESS.log(LOG, "IDE_HOME is set to {}", this.ideHome);
11✔
914
    }
915
  }
1✔
916

917
  @Override
918
  public String formatArgument(Object argument) {
919

920
    if (argument == null) {
2✔
921
      return null;
2✔
922
    }
923
    String result = argument.toString();
3✔
924
    if (isPrivacyMode()) {
3✔
925
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
926
        initializePrivacyMap(this.userHome, "~");
×
927
        String projectName = getProjectName();
×
928
        if (!projectName.isEmpty()) {
×
929
          this.privacyMap.put(projectName, "project");
×
930
        }
931
      }
932
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
933
        result = result.replace(entry.getKey(), entry.getValue());
×
934
      }
×
935
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
936
    }
937
    return result;
2✔
938
  }
939

940
  /**
941
   * @param path the sensitive {@link Path} to
942
   * @param replacement the replacement to mask the {@link Path} in log output.
943
   */
944
  protected void initializePrivacyMap(Path path, String replacement) {
945

946
    if (path == null) {
×
947
      return;
×
948
    }
949
    if (this.systemInfo.isWindows()) {
×
950
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
951
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
952
    } else {
953
      this.privacyMap.put(path.toString(), replacement);
×
954
    }
955
  }
×
956

957
  /**
958
   * Resets the privacy map in case fundamental values have changed.
959
   */
960
  private void resetPrivacyMap() {
961

962
    this.privacyMap.clear();
3✔
963
  }
1✔
964

965

966
  @Override
967
  public String askForInput(String message, String defaultValue) {
968

969
    while (true) {
970
      if (!message.isBlank()) {
3!
971
        IdeLogLevel.INTERACTION.log(LOG, message);
4✔
972
      }
973
      if (isBatchMode()) {
3!
974
        if (isForceMode()) {
×
975
          return defaultValue;
×
976
        } else {
977
          throw new CliAbortException();
×
978
        }
979
      }
980
      String input = readLine().trim();
4✔
981
      if (!input.isEmpty()) {
3!
982
        return input;
2✔
983
      } else {
984
        if (defaultValue != null) {
×
985
          return defaultValue;
×
986
        }
987
      }
988
    }
×
989
  }
990

991
  @Override
992
  public <O> O question(O[] options, String question, Object... args) {
993

994
    assert (options.length > 0);
4!
995
    IdeLogLevel.INTERACTION.log(LOG, question, args);
5✔
996
    return displayOptionsAndGetAnswer(options);
4✔
997
  }
998

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

1033
  private static String computeOptionKey(String option) {
1034
    String key = option;
2✔
1035
    int index = -1;
2✔
1036
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
1037
      int currentIndex = key.indexOf(c);
4✔
1038
      if (currentIndex != -1) {
3✔
1039
        if ((index == -1) || (currentIndex < index)) {
3!
1040
          index = currentIndex;
2✔
1041
        }
1042
      }
1043
    }
1044
    if (index > 0) {
2✔
1045
      key = key.substring(0, index).trim();
6✔
1046
    }
1047
    return key;
2✔
1048
  }
1049

1050
  /**
1051
   * @return the input from the end-user (e.g. read from the console).
1052
   */
1053
  protected abstract String readLine();
1054

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

1057
    O duplicate = mapping.put(key, option);
5✔
1058
    if (duplicate != null) {
2!
1059
      throw new IllegalArgumentException("Duplicated option " + key);
×
1060
    }
1061
  }
1✔
1062

1063
  @Override
1064
  public Step getCurrentStep() {
1065

1066
    return this.currentStep;
×
1067
  }
1068

1069
  @Override
1070
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1071

1072
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1073
    return this.currentStep;
3✔
1074
  }
1075

1076
  /**
1077
   * Internal method to end the running {@link Step}.
1078
   *
1079
   * @param step the current {@link Step} to end.
1080
   */
1081
  public void endStep(StepImpl step) {
1082

1083
    if (step == this.currentStep) {
4!
1084
      this.currentStep = this.currentStep.getParent();
6✔
1085
    } else {
1086
      String currentStepName = "null";
×
1087
      if (this.currentStep != null) {
×
1088
        currentStepName = this.currentStep.getName();
×
1089
      }
1090
      LOG.warn("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1091
    }
1092
  }
1✔
1093

1094
  /**
1095
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1096
   *
1097
   * @param arguments the {@link CliArgument}.
1098
   * @return the return code of the execution.
1099
   */
1100
  public int run(CliArguments arguments) {
1101

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

1145
  /**
1146
   * Ensure the logging system is initialized.
1147
   */
1148
  private void activateLogging(Commandlet cmd) {
1149

1150
    configureJavaUtilLogging(cmd);
3✔
1151
    this.startContext.activateLogging();
3✔
1152
  }
1✔
1153

1154
  /**
1155
   * Configures the logging system (JUL).
1156
   *
1157
   * @param cmd the {@link Commandlet} to be called. May be {@code null}.
1158
   */
1159
  public void configureJavaUtilLogging(Commandlet cmd) {
1160

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

1180
  protected boolean isWriteLogfile(Commandlet cmd) {
1181
    if (!cmd.isWriteLogFile()) {
×
1182
      return false;
×
1183
    }
1184
    Boolean writeLogfile = IdeVariables.IDE_WRITE_LOGFILE.get(this);
×
1185
    return Boolean.TRUE.equals(writeLogfile);
×
1186
  }
1187

1188
  private Properties createJavaUtilLoggingProperties(boolean writeLogfile, Commandlet cmd) {
1189

1190
    Path idePath = getIdePath();
3✔
1191
    if (writeLogfile && (idePath == null)) {
2!
1192
      writeLogfile = false;
×
1193
      LOG.error("Cannot enable log-file since IDE_ROOT is undefined.");
×
1194
    }
1195
    Properties properties = new Properties();
4✔
1196
    // prevent 3rd party (e.g. java.lang.ProcessBuilder) logging into our console via JUL
1197
    // see JulLogLevel for the trick we did to workaround JUL flaws
1198
    properties.setProperty(".level", "SEVERE");
5✔
1199
    if (writeLogfile) {
2!
1200
      this.startContext.setLogLevelLogger(IdeLogLevel.TRACE);
×
1201
      properties.setProperty("handlers", "com.devonfw.tools.ide.log.JulConsoleHandler,java.util.logging.FileHandler");
×
1202
      properties.setProperty("java.util.logging.FileHandler.formatter", "java.util.logging.SimpleFormatter");
×
1203
      properties.setProperty("java.util.logging.FileHandler.encoding", "UTF-8");
×
1204
      this.logfile = createLogfilePath(idePath, cmd);
×
1205
      getFileAccess().mkdirs(this.logfile.getParent());
×
1206
      properties.setProperty("java.util.logging.FileHandler.pattern", this.logfile.toString());
×
1207
    } else {
1208
      properties.setProperty("handlers", "com.devonfw.tools.ide.log.JulConsoleHandler");
5✔
1209
    }
1210
    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✔
1211
    return properties;
2✔
1212
  }
1213

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

1234
  @Override
1235
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1236

1237
    this.startContext.deactivateLogging(threshold);
4✔
1238
    lambda.run();
2✔
1239
    this.startContext.activateLogging();
3✔
1240
  }
1✔
1241

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

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

1303
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1304

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

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

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

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

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

1411
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1412

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

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

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

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

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

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

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

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

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

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

1658
  @Override
1659
  public WindowsPathSyntax getPathSyntax() {
1660

1661
    return this.pathSyntax;
3✔
1662
  }
1663

1664
  /**
1665
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1666
   */
1667
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1668

1669
    this.pathSyntax = pathSyntax;
3✔
1670
  }
1✔
1671

1672
  /**
1673
   * @return the {@link IdeStartContextImpl}.
1674
   */
1675
  public IdeStartContextImpl getStartContext() {
1676

1677
    return startContext;
3✔
1678
  }
1679

1680
  /**
1681
   * @return the {@link WindowsHelper}.
1682
   */
1683
  public final WindowsHelper getWindowsHelper() {
1684

1685
    if (this.windowsHelper == null) {
3✔
1686
      this.windowsHelper = createWindowsHelper();
4✔
1687
    }
1688
    return this.windowsHelper;
3✔
1689
  }
1690

1691
  /**
1692
   * @return the new {@link WindowsHelper} instance.
1693
   */
1694
  protected WindowsHelper createWindowsHelper() {
1695

1696
    return new WindowsHelperImpl(this);
×
1697
  }
1698

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

1704
    this.variables = null;
3✔
1705
    this.customToolRepository = null;
3✔
1706
  }
1✔
1707

1708
  @Override
1709
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1710

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

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

1722
  }
1723

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

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