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

devonfw / IDEasy / 19833758920

01 Dec 2025 06:47PM UTC coverage: 69.787% (-0.07%) from 69.854%
19833758920

push

github

web-flow
#1521: Use wiremock for npm repository. (#1529)

Co-authored-by: jan-vcapgemini <59438728+jan-vcapgemini@users.noreply.github.com>
Co-authored-by: jan-vcapgemini <jan-vincent.hoelzle@capgemini.com>
Co-authored-by: Jörg Hohwiller <hohwille@users.noreply.github.com>
Co-authored-by: Malte Brunnlieb <maybeec@users.noreply.github.com>

3822 of 6009 branches covered (63.6%)

Branch coverage included in aggregate %.

9790 of 13496 relevant lines covered (72.54%)

3.16 hits per line

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

67.81
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.nio.file.Files;
6
import java.nio.file.Path;
7
import java.time.LocalDateTime;
8
import java.util.ArrayList;
9
import java.util.HashMap;
10
import java.util.Iterator;
11
import java.util.List;
12
import java.util.Locale;
13
import java.util.Map;
14
import java.util.Map.Entry;
15
import java.util.Objects;
16
import java.util.function.Predicate;
17

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

77
/**
78
 * Abstract base implementation of {@link IdeContext}.
79
 */
80
public abstract class AbstractIdeContext implements IdeContext, IdeLogArgFormatter {
81

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

84
  private static final String LICENSE_URL = "https://github.com/devonfw/IDEasy/blob/main/documentation/LICENSE.adoc";
85
  public static final String BASH = "bash";
86
  private static final String DEFAULT_WINDOWS_GIT_PATH = "C:\\Program Files\\Git\\bin\\bash.exe";
87

88
  private static final String OPTION_DETAILS_START = "([";
89

90
  private final IdeStartContextImpl startContext;
91

92
  private Path ideHome;
93

94
  private final Path ideRoot;
95

96
  private Path confPath;
97

98
  protected Path settingsPath;
99

100
  private Path settingsCommitIdPath;
101

102
  protected Path pluginsPath;
103

104
  private Path workspacePath;
105

106
  private String workspaceName;
107

108
  private Path cwd;
109

110
  private Path downloadPath;
111

112
  private Path userHome;
113

114
  private Path userHomeIde;
115

116
  private SystemPath path;
117

118
  private WindowsPathSyntax pathSyntax;
119

120
  private final SystemInfo systemInfo;
121

122
  private EnvironmentVariables variables;
123

124
  private final FileAccess fileAccess;
125

126
  protected CommandletManager commandletManager;
127

128
  protected ToolRepository defaultToolRepository;
129

130
  private CustomToolRepository customToolRepository;
131

132
  private MvnRepository mvnRepository;
133

134
  private NpmRepository npmRepository;
135

136
  private DirectoryMerger workspaceMerger;
137

138
  protected UrlMetadata urlMetadata;
139

140
  protected Path defaultExecutionDirectory;
141

142
  private StepImpl currentStep;
143

144
  private NetworkStatus networkStatus;
145

146
  protected IdeSystem system;
147

148
  private WindowsHelper windowsHelper;
149

150
  private final Map<String, String> privacyMap;
151

152
  private Path bash;
153

154
  /**
155
   * The constructor.
156
   *
157
   * @param startContext the {@link IdeLogger}.
158
   * @param workingDirectory the optional {@link Path} to current working directory.
159
   */
160
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
161

162
    super();
2✔
163
    this.startContext = startContext;
3✔
164
    this.startContext.setArgFormatter(this);
4✔
165
    this.privacyMap = new HashMap<>();
5✔
166
    this.systemInfo = SystemInfoImpl.INSTANCE;
3✔
167
    this.commandletManager = new CommandletManagerImpl(this);
6✔
168
    this.fileAccess = new FileAccessImpl(this);
6✔
169
    String userHomeProperty = getSystem().getProperty("user.home");
5✔
170
    if (userHomeProperty != null) {
2!
171
      this.userHome = Path.of(userHomeProperty);
×
172
    }
173
    if (workingDirectory == null) {
2!
174
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
175
    }
176
    workingDirectory = workingDirectory.toAbsolutePath();
3✔
177
    if (Files.isDirectory(workingDirectory)) {
5✔
178
      workingDirectory = this.fileAccess.toCanonicalPath(workingDirectory);
6✔
179
    } else {
180
      warning("Current working directory does not exist: {}", workingDirectory);
9✔
181
    }
182
    this.cwd = workingDirectory;
3✔
183
    // detect IDE_HOME and WORKSPACE
184
    String workspace = null;
2✔
185
    Path ideHomeDir = null;
2✔
186
    IdeHomeAndWorkspace ideHomeAndWorkspace = findIdeHome(workingDirectory);
4✔
187
    if (ideHomeAndWorkspace != null) {
2!
188
      ideHomeDir = ideHomeAndWorkspace.home();
3✔
189
      workspace = ideHomeAndWorkspace.workspace();
3✔
190
    }
191

192
    // detection completed, initializing variables
193
    this.ideRoot = findIdeRoot(ideHomeDir);
5✔
194

195
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
196

197
    if (this.ideRoot != null) {
3✔
198
      Path tempDownloadPath = getTempDownloadPath();
3✔
199
      if (Files.isDirectory(tempDownloadPath)) {
6✔
200
        // TODO delete all files older than 1 day here...
201
      } else {
202
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
203
      }
204
    }
205

206
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
207
  }
1✔
208

209
  /**
210
   * 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
211
   * add additional validation or boundary checks.
212
   *
213
   * @param workingDirectory the starting directory for the search.
214
   * @return an instance of {@link IdeHomeAndWorkspace} where the IDE_HOME was found or {@code null} if not found.
215
   */
216
  protected IdeHomeAndWorkspace findIdeHome(Path workingDirectory) {
217

218
    Path currentDir = workingDirectory;
2✔
219
    String name1 = "";
2✔
220
    String name2 = "";
2✔
221
    String workspace = WORKSPACE_MAIN;
2✔
222
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
223

224
    while (currentDir != null) {
2✔
225
      trace("Looking for IDE_HOME in {}", currentDir);
9✔
226
      if (isIdeHome(currentDir)) {
4✔
227
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
228
          workspace = name2;
3✔
229
        }
230
        break;
231
      }
232
      name2 = name1;
2✔
233
      int nameCount = currentDir.getNameCount();
3✔
234
      if (nameCount >= 1) {
3✔
235
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
236
      }
237
      currentDir = currentDir.getParent();
3✔
238
      if ((ideRootPath != null) && (ideRootPath.equals(currentDir))) {
2!
239
        // prevent that during tests we traverse to the real IDE project of IDEasy developer
240
        currentDir = null;
×
241
      }
242
    }
1✔
243

244
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
245
  }
246

247
  /**
248
   * @return a new {@link MvnRepository}
249
   */
250
  protected MvnRepository createMvnRepository() {
251
    return new MvnRepository(this);
5✔
252
  }
253

254
  /**
255
   * @return a new {@link NpmRepository}
256
   */
257
  protected NpmRepository createNpmRepository() {
258
    return new NpmRepository(this);
×
259
  }
260

261
  private Path findIdeRoot(Path ideHomePath) {
262

263
    Path ideRootPath = null;
2✔
264
    if (ideHomePath != null) {
2✔
265
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
266
      ideRootPath = ideHomePath.getParent();
3✔
267
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
2!
268
        warning(
×
269
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
270
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
271
            ideRootPathFromEnv,
272
            ideHomePath.getFileName(), ideRootPath);
×
273
      }
274
    } else if (!isTest()) {
4!
275
      ideRootPath = getIdeRootPathFromEnv(true);
×
276
    }
277
    return ideRootPath;
2✔
278
  }
279

280
  /**
281
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
282
   */
283
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
284

285
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
286
    if (root != null) {
×
287
      Path rootPath = Path.of(root);
×
288
      if (Files.isDirectory(rootPath)) {
×
289
        Path absoluteRootPath = getFileAccess().toCanonicalPath(rootPath);
×
290
        if (withSanityCheck) {
×
291
          int nameCount = rootPath.getNameCount();
×
292
          int absoluteNameCount = absoluteRootPath.getNameCount();
×
293
          int delta = absoluteNameCount - nameCount;
×
294
          if (delta >= 0) {
×
295
            for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) {
×
296
              String rootName = rootPath.getName(nameIndex).toString();
×
297
              String absoluteRootName = absoluteRootPath.getName(nameIndex + delta).toString();
×
298
              if (!rootName.equals(absoluteRootName)) {
×
299
                warning("IDE_ROOT is set to {} but was expanded to absolute path {} and does not match for segment {} and {} - fix your IDEasy installation!",
×
300
                    rootPath, absoluteRootPath, rootName, absoluteRootName);
301
                break;
×
302
              }
303
            }
304
          } else {
305
            warning("IDE_ROOT is set to {} but was expanded to a shorter absolute path {}", rootPath,
×
306
                absoluteRootPath);
307
          }
308
        }
309
        return absoluteRootPath;
×
310
      } else if (withSanityCheck) {
×
311
        warning("IDE_ROOT is set to {} that is not an existing directory - fix your IDEasy installation!", rootPath);
×
312
      }
313
    }
314
    return null;
×
315
  }
316

317
  @Override
318
  public void setCwd(Path userDir, String workspace, Path ideHome) {
319

320
    this.cwd = userDir;
3✔
321
    this.workspaceName = workspace;
3✔
322
    this.ideHome = ideHome;
3✔
323
    if (ideHome == null) {
2✔
324
      this.workspacePath = null;
3✔
325
      this.confPath = null;
3✔
326
      this.settingsPath = null;
3✔
327
      this.pluginsPath = null;
4✔
328
    } else {
329
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
330
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
331
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
332
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
333
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
334
    }
335
    if (isTest()) {
3!
336
      // only for testing...
337
      if (this.ideHome == null) {
3✔
338
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
339
      } else {
340
        this.userHome = this.ideHome.resolve("home");
6✔
341
      }
342
    }
343
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
344
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
345
    resetPrivacyMap();
2✔
346
    this.path = computeSystemPath();
4✔
347
  }
1✔
348

349
  private String getMessageIdeHomeFound() {
350

351
    String wks = this.workspaceName;
3✔
352
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
353
      wks = "*".repeat(wks.length());
×
354
    }
355
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
356
  }
357

358
  private String getMessageNotInsideIdeProject() {
359

360
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
361
  }
362

363
  private String getMessageIdeRootNotFound() {
364

365
    String root = getSystem().getEnv("IDE_ROOT");
5✔
366
    if (root == null) {
2!
367
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
368
    } else {
369
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
370
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
371
    }
372
  }
373

374
  /**
375
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
376
   */
377
  public boolean isTest() {
378

379
    return false;
×
380
  }
381

382
  protected SystemPath computeSystemPath() {
383

384
    return new SystemPath(this);
×
385
  }
386

387
  /**
388
   * Checks if the given directory is a valid IDE home by verifying it contains both 'workspaces' and 'settings' directories.
389
   *
390
   * @param dir the directory to check.
391
   * @return {@code true} if the directory is a valid IDE home, {@code false} otherwise.
392
   */
393
  protected boolean isIdeHome(Path dir) {
394

395
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
396
      return false;
2✔
397
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
398
      return false;
×
399
    }
400
    return true;
2✔
401
  }
402

403
  private EnvironmentVariables createVariables() {
404

405
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
406
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
407
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
408
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
409
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
410
    return conf.resolved();
3✔
411
  }
412

413
  protected AbstractEnvironmentVariables createSystemVariables() {
414

415
    return EnvironmentVariables.ofSystem(this);
3✔
416
  }
417

418
  @Override
419
  public SystemInfo getSystemInfo() {
420

421
    return this.systemInfo;
3✔
422
  }
423

424
  @Override
425
  public FileAccess getFileAccess() {
426

427
    return this.fileAccess;
3✔
428
  }
429

430
  @Override
431
  public CommandletManager getCommandletManager() {
432

433
    return this.commandletManager;
3✔
434
  }
435

436
  @Override
437
  public ToolRepository getDefaultToolRepository() {
438

439
    return this.defaultToolRepository;
3✔
440
  }
441

442
  @Override
443
  public MvnRepository getMvnRepository() {
444
    if (this.mvnRepository == null) {
3✔
445
      this.mvnRepository = createMvnRepository();
4✔
446
    }
447
    return this.mvnRepository;
3✔
448
  }
449

450
  @Override
451
  public NpmRepository getNpmRepository() {
452
    if (this.npmRepository == null) {
3✔
453
      this.npmRepository = createNpmRepository();
4✔
454
    }
455
    return this.npmRepository;
3✔
456
  }
457

458
  @Override
459
  public CustomToolRepository getCustomToolRepository() {
460

461
    if (this.customToolRepository == null) {
3!
462
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
463
    }
464
    return this.customToolRepository;
3✔
465
  }
466

467
  @Override
468
  public Path getIdeHome() {
469

470
    return this.ideHome;
3✔
471
  }
472

473
  @Override
474
  public String getProjectName() {
475

476
    if (this.ideHome != null) {
3!
477
      return this.ideHome.getFileName().toString();
5✔
478
    }
479
    return "";
×
480
  }
481

482
  @Override
483
  public VersionIdentifier getProjectVersion() {
484

485
    if (this.ideHome != null) {
3!
486
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
487
      if (Files.exists(versionFile)) {
5✔
488
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
489
        return VersionIdentifier.of(version);
3✔
490
      }
491
    }
492
    return IdeMigrator.START_VERSION;
2✔
493
  }
494

495
  @Override
496
  public void setProjectVersion(VersionIdentifier version) {
497

498
    if (this.ideHome == null) {
3!
499
      throw new IllegalStateException("IDE_HOME not available!");
×
500
    }
501
    Objects.requireNonNull(version);
3✔
502
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
503
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
504
  }
1✔
505

506
  @Override
507
  public Path getIdeRoot() {
508

509
    return this.ideRoot;
3✔
510
  }
511

512
  @Override
513
  public Path getIdePath() {
514

515
    Path myIdeRoot = getIdeRoot();
3✔
516
    if (myIdeRoot == null) {
2✔
517
      return null;
2✔
518
    }
519
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
520
  }
521

522
  @Override
523
  public Path getCwd() {
524

525
    return this.cwd;
3✔
526
  }
527

528
  @Override
529
  public Path getTempPath() {
530

531
    Path idePath = getIdePath();
3✔
532
    if (idePath == null) {
2!
533
      return null;
×
534
    }
535
    return idePath.resolve("tmp");
4✔
536
  }
537

538
  @Override
539
  public Path getTempDownloadPath() {
540

541
    Path tmp = getTempPath();
3✔
542
    if (tmp == null) {
2!
543
      return null;
×
544
    }
545
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
546
  }
547

548
  @Override
549
  public Path getUserHome() {
550

551
    return this.userHome;
3✔
552
  }
553

554
  /**
555
   * This method should only be used for tests to mock user home.
556
   *
557
   * @param userHome the new value of {@link #getUserHome()}.
558
   */
559
  protected void setUserHome(Path userHome) {
560

561
    this.userHome = userHome;
3✔
562
    resetPrivacyMap();
2✔
563
  }
1✔
564

565
  @Override
566
  public Path getUserHomeIde() {
567

568
    return this.userHomeIde;
3✔
569
  }
570

571
  @Override
572
  public Path getSettingsPath() {
573

574
    return this.settingsPath;
3✔
575
  }
576

577
  @Override
578
  public Path getSettingsGitRepository() {
579

580
    Path settingsPath = getSettingsPath();
3✔
581
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
582
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
583
      error("Settings repository exists but is not a git repository.");
3✔
584
      return null;
2✔
585
    }
586
    return settingsPath;
2✔
587
  }
588

589
  @Override
590
  public boolean isSettingsRepositorySymlinkOrJunction() {
591

592
    Path settingsPath = getSettingsPath();
3✔
593
    if (settingsPath == null) {
2!
594
      return false;
×
595
    }
596
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
597
  }
598

599
  @Override
600
  public Path getSettingsCommitIdPath() {
601

602
    return this.settingsCommitIdPath;
3✔
603
  }
604

605
  @Override
606
  public Path getConfPath() {
607

608
    return this.confPath;
3✔
609
  }
610

611
  @Override
612
  public Path getSoftwarePath() {
613

614
    if (this.ideHome == null) {
3✔
615
      return null;
2✔
616
    }
617
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
618
  }
619

620
  @Override
621
  public Path getSoftwareExtraPath() {
622

623
    Path softwarePath = getSoftwarePath();
3✔
624
    if (softwarePath == null) {
2!
625
      return null;
×
626
    }
627
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
628
  }
629

630
  @Override
631
  public Path getSoftwareRepositoryPath() {
632

633
    Path idePath = getIdePath();
3✔
634
    if (idePath == null) {
2✔
635
      return null;
2✔
636
    }
637
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
638
  }
639

640
  @Override
641
  public Path getPluginsPath() {
642

643
    return this.pluginsPath;
3✔
644
  }
645

646
  @Override
647
  public String getWorkspaceName() {
648

649
    return this.workspaceName;
3✔
650
  }
651

652
  @Override
653
  public Path getWorkspacePath() {
654

655
    return this.workspacePath;
3✔
656
  }
657

658
  @Override
659
  public Path getDownloadPath() {
660

661
    return this.downloadPath;
3✔
662
  }
663

664
  @Override
665
  public Path getUrlsPath() {
666

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

674
  @Override
675
  public Path getToolRepositoryPath() {
676

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

684
  @Override
685
  public SystemPath getPath() {
686

687
    return this.path;
3✔
688
  }
689

690
  @Override
691
  public EnvironmentVariables getVariables() {
692

693
    if (this.variables == null) {
3✔
694
      this.variables = createVariables();
4✔
695
    }
696
    return this.variables;
3✔
697
  }
698

699
  @Override
700
  public UrlMetadata getUrls() {
701

702
    if (this.urlMetadata == null) {
3✔
703
      if (!isTest()) {
3!
704
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
705
      }
706
      this.urlMetadata = new UrlMetadata(this);
6✔
707
    }
708
    return this.urlMetadata;
3✔
709
  }
710

711
  @Override
712
  public boolean isQuietMode() {
713

714
    return this.startContext.isQuietMode();
4✔
715
  }
716

717
  @Override
718
  public boolean isBatchMode() {
719

720
    return this.startContext.isBatchMode();
4✔
721
  }
722

723
  @Override
724
  public boolean isForceMode() {
725

726
    return this.startContext.isForceMode();
4✔
727
  }
728

729
  @Override
730
  public boolean isForcePull() {
731

732
    return this.startContext.isForcePull();
4✔
733
  }
734

735
  @Override
736
  public boolean isForcePlugins() {
737

738
    return this.startContext.isForcePlugins();
4✔
739
  }
740

741
  @Override
742
  public boolean isForceRepositories() {
743

744
    return this.startContext.isForceRepositories();
4✔
745
  }
746

747
  @Override
748
  public boolean isOfflineMode() {
749

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

753
  @Override
754
  public boolean isPrivacyMode() {
755
    return this.startContext.isPrivacyMode();
4✔
756
  }
757

758
  @Override
759
  public boolean isSkipUpdatesMode() {
760

761
    return this.startContext.isSkipUpdatesMode();
4✔
762
  }
763

764
  @Override
765
  public boolean isNoColorsMode() {
766

767
    return this.startContext.isNoColorsMode();
×
768
  }
769

770
  @Override
771
  public NetworkStatus getNetworkStatus() {
772

773
    if (this.networkStatus == null) {
×
774
      this.networkStatus = new NetworkStatusImpl(this);
×
775
    }
776
    return this.networkStatus;
×
777
  }
778

779
  @Override
780
  public Locale getLocale() {
781

782
    Locale locale = this.startContext.getLocale();
4✔
783
    if (locale == null) {
2✔
784
      locale = Locale.getDefault();
2✔
785
    }
786
    return locale;
2✔
787
  }
788

789
  @Override
790
  public DirectoryMerger getWorkspaceMerger() {
791

792
    if (this.workspaceMerger == null) {
3✔
793
      this.workspaceMerger = new DirectoryMerger(this);
6✔
794
    }
795
    return this.workspaceMerger;
3✔
796
  }
797

798
  /**
799
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
800
   */
801
  @Override
802
  public Path getDefaultExecutionDirectory() {
803

804
    return this.defaultExecutionDirectory;
×
805
  }
806

807
  /**
808
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
809
   */
810
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
811

812
    if (defaultExecutionDirectory != null) {
×
813
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
814
    }
815
  }
×
816

817
  @Override
818
  public GitContext getGitContext() {
819

820
    return new GitContextImpl(this);
×
821
  }
822

823
  @Override
824
  public ProcessContext newProcess() {
825

826
    ProcessContext processContext = createProcessContext();
3✔
827
    if (this.defaultExecutionDirectory != null) {
3!
828
      processContext.directory(this.defaultExecutionDirectory);
×
829
    }
830
    return processContext;
2✔
831
  }
832

833
  @Override
834
  public IdeSystem getSystem() {
835

836
    if (this.system == null) {
×
837
      this.system = new IdeSystemImpl(this);
×
838
    }
839
    return this.system;
×
840
  }
841

842
  /**
843
   * @return a new instance of {@link ProcessContext}.
844
   * @see #newProcess()
845
   */
846
  protected ProcessContext createProcessContext() {
847

848
    return new ProcessContextImpl(this);
5✔
849
  }
850

851
  @Override
852
  public IdeSubLogger level(IdeLogLevel level) {
853

854
    return this.startContext.level(level);
5✔
855
  }
856

857
  @Override
858
  public void logIdeHomeAndRootStatus() {
859
    if (this.ideRoot != null) {
3!
860
      success("IDE_ROOT is set to {}", this.ideRoot);
×
861
    }
862
    if (this.ideHome == null) {
3✔
863
      warning(getMessageNotInsideIdeProject());
5✔
864
    } else {
865
      success("IDE_HOME is set to {}", this.ideHome);
10✔
866
    }
867
  }
1✔
868

869
  @Override
870
  public String formatArgument(Object argument) {
871

872
    if (argument == null) {
2✔
873
      return null;
2✔
874
    }
875
    String result = argument.toString();
3✔
876
    if (isPrivacyMode()) {
3✔
877
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
878
        initializePrivacyMap(this.userHome, "~");
×
879
        String projectName = getProjectName();
×
880
        if (!projectName.isEmpty()) {
×
881
          this.privacyMap.put(projectName, "project");
×
882
        }
883
      }
884
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
885
        result = result.replace(entry.getKey(), entry.getValue());
×
886
      }
×
887
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
888
    }
889
    return result;
2✔
890
  }
891

892
  /**
893
   * @param path the sensitive {@link Path} to
894
   * @param replacement the replacement to mask the {@link Path} in log output.
895
   */
896
  protected void initializePrivacyMap(Path path, String replacement) {
897

898
    if (path == null) {
×
899
      return;
×
900
    }
901
    if (this.systemInfo.isWindows()) {
×
902
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
903
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
904
    } else {
905
      this.privacyMap.put(path.toString(), replacement);
×
906
    }
907
  }
×
908

909
  /**
910
   * Resets the privacy map in case fundamental values have changed.
911
   */
912
  private void resetPrivacyMap() {
913

914
    this.privacyMap.clear();
3✔
915
  }
1✔
916

917

918
  @Override
919
  public String askForInput(String message, String defaultValue) {
920

921
    while (true) {
922
      if (!message.isBlank()) {
3!
923
        interaction(message);
3✔
924
      }
925
      if (isBatchMode()) {
3!
926
        if (isForceMode()) {
×
927
          return defaultValue;
×
928
        } else {
929
          throw new CliAbortException();
×
930
        }
931
      }
932
      String input = readLine().trim();
4✔
933
      if (!input.isEmpty()) {
3!
934
        return input;
2✔
935
      } else {
936
        if (defaultValue != null) {
×
937
          return defaultValue;
×
938
        }
939
      }
940
    }
×
941
  }
942

943
  @SuppressWarnings("unchecked")
944
  @Override
945
  public <O> O question(O[] options, String question, Object... args) {
946

947
    assert (options.length > 0);
4!
948
    interaction(question, args);
4✔
949
    return displayOptionsAndGetAnswer(options);
4✔
950
  }
951

952
  private <O> O displayOptionsAndGetAnswer(O[] options) {
953
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
954
    int i = 0;
2✔
955
    for (O option : options) {
16✔
956
      i++;
1✔
957
      String title = "" + option;
4✔
958
      String key = computeOptionKey(title);
3✔
959
      addMapping(mapping, key, option);
4✔
960
      String numericKey = Integer.toString(i);
3✔
961
      if (numericKey.equals(key)) {
4!
962
        trace("Options should not be numeric: " + key);
×
963
      } else {
964
        addMapping(mapping, numericKey, option);
4✔
965
      }
966
      interaction("Option " + numericKey + ": " + title);
5✔
967
    }
968
    O option = null;
2✔
969
    if (isBatchMode()) {
3!
970
      if (isForceMode()) {
×
971
        option = options[0];
×
972
        interaction("" + option);
×
973
      }
974
    } else {
975
      while (option == null) {
2✔
976
        String answer = readLine();
3✔
977
        option = mapping.get(answer);
4✔
978
        if (option == null) {
2!
979
          warning("Invalid answer: '" + answer + "' - please try again.");
×
980
        }
981
      }
1✔
982
    }
983
    return option;
2✔
984
  }
985

986
  private static String computeOptionKey(String option) {
987
    String key = option;
2✔
988
    int index = -1;
2✔
989
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
990
      int currentIndex = key.indexOf(c);
4✔
991
      if (currentIndex != -1) {
3✔
992
        if ((index == -1) || (currentIndex < index)) {
3!
993
          index = currentIndex;
2✔
994
        }
995
      }
996
    }
997
    if (index > 0) {
2✔
998
      key = key.substring(0, index).trim();
6✔
999
    }
1000
    return key;
2✔
1001
  }
1002

1003
  /**
1004
   * @return the input from the end-user (e.g. read from the console).
1005
   */
1006
  protected abstract String readLine();
1007

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

1010
    O duplicate = mapping.put(key, option);
5✔
1011
    if (duplicate != null) {
2!
1012
      throw new IllegalArgumentException("Duplicated option " + key);
×
1013
    }
1014
  }
1✔
1015

1016
  @Override
1017
  public Step getCurrentStep() {
1018

1019
    return this.currentStep;
×
1020
  }
1021

1022
  @Override
1023
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1024

1025
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1026
    return this.currentStep;
3✔
1027
  }
1028

1029
  /**
1030
   * Internal method to end the running {@link Step}.
1031
   *
1032
   * @param step the current {@link Step} to end.
1033
   */
1034
  public void endStep(StepImpl step) {
1035

1036
    if (step == this.currentStep) {
4!
1037
      this.currentStep = this.currentStep.getParent();
6✔
1038
    } else {
1039
      String currentStepName = "null";
×
1040
      if (this.currentStep != null) {
×
1041
        currentStepName = this.currentStep.getName();
×
1042
      }
1043
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1044
    }
1045
  }
1✔
1046

1047
  /**
1048
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1049
   *
1050
   * @param arguments the {@link CliArgument}.
1051
   * @return the return code of the execution.
1052
   */
1053
  public int run(CliArguments arguments) {
1054

1055
    CliArgument current = arguments.current();
3✔
1056
    assert (this.currentStep == null);
4!
1057
    boolean supressStepSuccess = false;
2✔
1058
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1059
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1060
    Commandlet cmd = null;
2✔
1061
    ValidationResult result = null;
2✔
1062
    try {
1063
      while (commandletIterator.hasNext()) {
3✔
1064
        cmd = commandletIterator.next();
4✔
1065
        result = applyAndRun(arguments.copy(), cmd);
6✔
1066
        if (result.isValid()) {
3!
1067
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1068
          step.success();
2✔
1069
          return ProcessResult.SUCCESS;
4✔
1070
        }
1071
      }
1072
      this.startContext.activateLogging();
3✔
1073
      verifyIdeMinVersion(false);
3✔
1074
      if (result != null) {
2!
1075
        error(result.getErrorMessage());
×
1076
      }
1077
      step.error("Invalid arguments: {}", current.getArgs());
10✔
1078
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
1079
      if (cmd != null) {
2!
1080
        help.commandlet.setValue(cmd);
×
1081
      }
1082
      help.run();
2✔
1083
      return 1;
4✔
1084
    } catch (Throwable t) {
1✔
1085
      this.startContext.activateLogging();
3✔
1086
      step.error(t, true);
4✔
1087
      throw t;
2✔
1088
    } finally {
1089
      step.close();
2✔
1090
      assert (this.currentStep == null);
4!
1091
      step.logSummary(supressStepSuccess);
3✔
1092
    }
1093
  }
1094

1095
  @Override
1096
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1097

1098
    this.startContext.deactivateLogging(threshold);
4✔
1099
    lambda.run();
2✔
1100
    this.startContext.activateLogging();
3✔
1101
  }
1✔
1102

1103
  /**
1104
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1105
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1106
   *     {@link Commandlet} did not match and we have to try a different candidate).
1107
   */
1108
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1109

1110
    IdeLogLevel previousLogLevel = null;
2✔
1111
    cmd.reset();
2✔
1112
    ValidationResult result = apply(arguments, cmd);
5✔
1113
    if (result.isValid()) {
3!
1114
      result = cmd.validate();
3✔
1115
    }
1116
    if (result.isValid()) {
3!
1117
      debug("Running commandlet {}", cmd);
9✔
1118
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1119
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1120
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1121
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1122
      }
1123
      try {
1124
        if (cmd.isProcessableOutput()) {
3!
1125
          if (!debug().isEnabled()) {
×
1126
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1127
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
1128
          }
1129
          this.startContext.activateLogging();
×
1130
        } else {
1131
          this.startContext.activateLogging();
3✔
1132
          if (cmd.isIdeHomeRequired()) {
3!
1133
            debug(getMessageIdeHomeFound());
4✔
1134
          }
1135
          Path settingsRepository = getSettingsGitRepository();
3✔
1136
          if (settingsRepository != null) {
2!
1137
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1138
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1139
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1140
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1141
                interaction(
×
1142
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
1143

1144
              } else {
1145
                interaction(
×
1146
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
1147
              }
1148
            }
1149
          }
1150
        }
1151
        boolean success = ensureLicenseAgreement(cmd);
4✔
1152
        if (!success) {
2!
1153
          return ValidationResultValid.get();
×
1154
        }
1155
        cmd.run();
2✔
1156
      } finally {
1157
        if (previousLogLevel != null) {
2!
1158
          this.startContext.setLogLevel(previousLogLevel);
×
1159
        }
1160
      }
1✔
1161
    } else {
1162
      trace("Commandlet did not match");
×
1163
    }
1164
    return result;
2✔
1165
  }
1166

1167
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1168

1169
    if (isTest()) {
3!
1170
      return true; // ignore for tests
2✔
1171
    }
1172
    getFileAccess().mkdirs(this.userHomeIde);
×
1173
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1174
    if (Files.isRegularFile(licenseAgreement)) {
×
1175
      return true; // success, license already accepted
×
1176
    }
1177
    if (cmd instanceof EnvironmentCommandlet) {
×
1178
      // 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
1179
      // 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
1180
      // printing anything anymore in such case.
1181
      return false;
×
1182
    }
1183
    boolean logLevelInfoDisabled = !this.startContext.info().isEnabled();
×
1184
    if (logLevelInfoDisabled) {
×
1185
      this.startContext.setLogLevel(IdeLogLevel.INFO, true);
×
1186
    }
1187
    boolean logLevelInteractionDisabled = !this.startContext.interaction().isEnabled();
×
1188
    if (logLevelInteractionDisabled) {
×
1189
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, true);
×
1190
    }
1191
    StringBuilder sb = new StringBuilder(1180);
×
1192
    sb.append(LOGO).append("""
×
1193
        Welcome to IDEasy!
1194
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1195
        It supports automatic download and installation of arbitrary 3rd party tools.
1196
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1197
        But if explicitly configured, also commercial software that requires an additional license may be used.
1198
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1199
        You are solely responsible for all risks implied by using this software.
1200
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1201
        You will be able to find it online under the following URL:
1202
        """).append(LICENSE_URL);
×
1203
    if (this.ideRoot != null) {
×
1204
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1205
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1206
    }
1207
    info(sb.toString());
×
1208
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1209

1210
    sb.setLength(0);
×
1211
    LocalDateTime now = LocalDateTime.now();
×
1212
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1213
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1214
    try {
1215
      Files.writeString(licenseAgreement, sb);
×
1216
    } catch (Exception e) {
×
1217
      throw new RuntimeException("Failed to save license agreement!", e);
×
1218
    }
×
1219
    if (logLevelInfoDisabled) {
×
1220
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
1221
    }
1222
    if (logLevelInteractionDisabled) {
×
1223
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
1224
    }
1225
    return true;
×
1226
  }
1227

1228
  @Override
1229
  public void verifyIdeMinVersion(boolean throwException) {
1230
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1231
    if (minVersion == null) {
2✔
1232
      return;
1✔
1233
    }
1234
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1235
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1236
          + "However, this is too old as your project requires at latest version %s\n"
1237
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1238
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1239
      if (throwException) {
2✔
1240
        throw new CliException(message);
5✔
1241
      } else {
1242
        warning(message);
3✔
1243
      }
1244
    }
1245
  }
1✔
1246

1247
  /**
1248
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1249
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1250
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1251
   */
1252
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1253

1254
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1255
    if (arguments.current().isStart()) {
4✔
1256
      arguments.next();
3✔
1257
    }
1258
    if (includeContextOptions) {
2✔
1259
      ContextCommandlet cc = new ContextCommandlet();
4✔
1260
      for (Property<?> property : cc.getProperties()) {
11✔
1261
        assert (property.isOption());
4!
1262
        property.apply(arguments, this, cc, collector);
7✔
1263
      }
1✔
1264
    }
1265
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1266
    CliArgument current = arguments.current();
3✔
1267
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1268
      collector.add(current.get(), null, null, null);
7✔
1269
    }
1270
    arguments.next();
3✔
1271
    while (commandletIterator.hasNext()) {
3✔
1272
      Commandlet cmd = commandletIterator.next();
4✔
1273
      if (!arguments.current().isEnd()) {
4✔
1274
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1275
      }
1276
    }
1✔
1277
    return collector.getSortedCandidates();
3✔
1278
  }
1279

1280
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1281

1282
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
1283
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1284
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1285
    List<Property<?>> properties = cmd.getProperties();
3✔
1286
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1287
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1288
    for (Property<?> property : properties) {
10✔
1289
      if (property.isOption()) {
3✔
1290
        optionProperties.add(property);
4✔
1291
      }
1292
    }
1✔
1293
    CliArgument currentArgument = arguments.current();
3✔
1294
    while (!currentArgument.isEnd()) {
3✔
1295
      trace("Trying to match argument '{}'", currentArgument);
9✔
1296
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1297
        if (currentArgument.isCompletion()) {
3✔
1298
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1299
          while (optionIterator.hasNext()) {
3✔
1300
            Property<?> option = optionIterator.next();
4✔
1301
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1302
            if (success) {
2✔
1303
              optionIterator.remove();
2✔
1304
              arguments.next();
3✔
1305
            }
1306
          }
1✔
1307
        } else {
1✔
1308
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1309
          if (option != null) {
2✔
1310
            arguments.next();
3✔
1311
            boolean removed = optionProperties.remove(option);
4✔
1312
            if (!removed) {
2!
1313
              option = null;
×
1314
            }
1315
          }
1316
          if (option == null) {
2✔
1317
            trace("No such option was found.");
3✔
1318
            return;
1✔
1319
          }
1320
        }
1✔
1321
      } else {
1322
        if (valueIterator.hasNext()) {
3✔
1323
          Property<?> valueProperty = valueIterator.next();
4✔
1324
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1325
          if (!success) {
2✔
1326
            trace("Completion cannot match any further.");
3✔
1327
            return;
1✔
1328
          }
1329
        } else {
1✔
1330
          trace("No value left for completion.");
3✔
1331
          return;
1✔
1332
        }
1333
      }
1334
      currentArgument = arguments.current();
4✔
1335
    }
1336
  }
1✔
1337

1338
  /**
1339
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1340
   *     {@link CliArguments#copy() copy} as needed.
1341
   * @param cmd the potential {@link Commandlet} to match.
1342
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1343
   */
1344
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1345

1346
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1347
    CliArgument currentArgument = arguments.current();
3✔
1348
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1349
    Property<?> property = null;
2✔
1350
    if (propertyIterator.hasNext()) {
3!
1351
      property = propertyIterator.next();
4✔
1352
    }
1353
    while (!currentArgument.isEnd()) {
3✔
1354
      trace("Trying to match argument '{}'", currentArgument);
9✔
1355
      Property<?> currentProperty = property;
2✔
1356
      if (!arguments.isEndOptions()) {
3!
1357
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1358
        if (option != null) {
2!
1359
          currentProperty = option;
×
1360
        }
1361
      }
1362
      if (currentProperty == null) {
2!
1363
        trace("No option or next value found");
×
1364
        ValidationState state = new ValidationState(null);
×
1365
        state.addErrorMessage("No matching property found");
×
1366
        return state;
×
1367
      }
1368
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1369
      if (currentProperty == property) {
3!
1370
        if (!property.isMultiValued()) {
3✔
1371
          if (propertyIterator.hasNext()) {
3✔
1372
            property = propertyIterator.next();
5✔
1373
          } else {
1374
            property = null;
2✔
1375
          }
1376
        }
1377
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1378
          arguments.stopSplitShortOptions();
2✔
1379
        }
1380
      }
1381
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1382
      if (!matches) {
2!
1383
        ValidationState state = new ValidationState(null);
×
1384
        state.addErrorMessage("No matching property found");
×
1385
        return state;
×
1386
      }
1387
      currentArgument = arguments.current();
3✔
1388
    }
1✔
1389
    return ValidationResultValid.get();
2✔
1390
  }
1391

1392
  @Override
1393
  public Path findBash() {
1394
    if (this.bash != null) {
3✔
1395
      return this.bash;
3✔
1396
    }
1397
    Path bashPath = findBashOnBashPath();
3✔
1398
    if (bashPath == null) {
2✔
1399
      bashPath = findBashInPath();
3✔
1400
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1401
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1402
        if (bashPath == null) {
2!
1403
          bashPath = findBashInWindowsRegistry();
3✔
1404
        }
1405
      }
1406
    }
1407
    if (bashPath == null) {
2✔
1408
      error("No bash executable could be found on your system.");
4✔
1409
    } else {
1410
      this.bash = bashPath;
3✔
1411
    }
1412
    return bashPath;
2✔
1413
  }
1414

1415
  private Path findBashOnBashPath() {
1416
    trace("Trying to find BASH_PATH environment variable.");
3✔
1417
    Path bash;
1418
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1419
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1420
    if (bashVariable != null) {
2✔
1421
      bash = Path.of(bashVariable);
5✔
1422
      if (Files.exists(bash)) {
5✔
1423
        debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
13✔
1424
        return bash;
2✔
1425
      } else {
1426
        error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
13✔
1427
        return null;
2✔
1428
      }
1429
    } else {
1430
      debug("{} environment variable was not found", bashPathVariableName);
9✔
1431
      return null;
2✔
1432
    }
1433
  }
1434

1435
  /**
1436
   * @param path the path to check.
1437
   * @param toIgnore the String sequence which needs to be checked and ignored.
1438
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1439
   */
1440
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1441
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1442
    return !s.contains(toIgnore);
7!
1443
  }
1444

1445
  /**
1446
   * Tries to find the bash.exe within the PATH environment variable.
1447
   *
1448
   * @return Path to bash.exe if found in PATH environment variable, {@code null} if bash.exe was not found.
1449
   */
1450
  private Path findBashInPath() {
1451
    trace("Trying to find bash in PATH environment variable.");
3✔
1452
    Path bash;
1453
    String pathVariableName = IdeVariables.PATH.getName();
3✔
1454
    if (pathVariableName != null) {
2!
1455
      Path plainBash = Path.of(BASH);
5✔
1456
      Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
16!
1457
          "\\windows\\system32");
1458
      Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
6✔
1459
      bash = bashPath.toAbsolutePath();
3✔
1460
      if (bashPath.equals(plainBash)) {
4✔
1461
        warning("No usable bash executable was found in your PATH environment variable!");
3✔
1462
        bash = null;
3✔
1463
      } else {
1464
        if (Files.exists(bashPath)) {
5!
1465
          debug("A proper bash executable was found in your PATH environment variable at: {}", bash);
10✔
1466
        } else {
1467
          bash = null;
×
1468
          error("A path to a bash executable was found in your PATH environment variable at: {} but the file is not existing.", bash);
×
1469
        }
1470
      }
1471
    } else {
1✔
1472
      bash = null;
×
1473
      // this should never happen...
1474
      error("PATH environment variable was not found");
×
1475
    }
1476
    return bash;
2✔
1477
  }
1478

1479
  /**
1480
   * Tries to find the bash.exe within the Windows registry.
1481
   *
1482
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1483
   */
1484
  protected Path findBashInWindowsRegistry() {
1485
    trace("Trying to find bash in Windows registry");
×
1486
    // If not found in the default location, try the registry query
1487
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1488
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1489
    for (String bashVariant : bashVariants) {
×
1490
      trace("Trying to find bash variant: {}", bashVariant);
×
1491
      for (String registryKey : registryKeys) {
×
1492
        trace("Trying to find bash from registry key: {}", registryKey);
×
1493
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1494
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1495

1496
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1497
        if (path != null) {
×
1498
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1499
          if (Files.exists(bashPath)) {
×
1500
            debug("Found bash at: {}", bashPath);
×
1501
            return bashPath;
×
1502
          } else {
1503
            error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1504
            return null;
×
1505
          }
1506
        } else {
1507
          info("No bash executable could be found in the Windows registry.");
×
1508
        }
1509
      }
1510
    }
1511
    // no bash found
1512
    return null;
×
1513
  }
1514

1515
  private Path findBashOnWindowsDefaultGitPath() {
1516
    // Check if Git Bash exists in the default location
1517
    trace("Trying to find bash on the Windows default git path.");
3✔
1518
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1519
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1520
      trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1521
      return defaultPath;
×
1522
    }
1523
    debug("No bash was found on the Windows default git path.");
3✔
1524
    return null;
2✔
1525
  }
1526

1527
  @Override
1528
  public WindowsPathSyntax getPathSyntax() {
1529

1530
    return this.pathSyntax;
3✔
1531
  }
1532

1533
  /**
1534
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1535
   */
1536
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1537

1538
    this.pathSyntax = pathSyntax;
3✔
1539
  }
1✔
1540

1541
  /**
1542
   * @return the {@link IdeStartContextImpl}.
1543
   */
1544
  public IdeStartContextImpl getStartContext() {
1545

1546
    return startContext;
3✔
1547
  }
1548

1549
  /**
1550
   * @return the {@link WindowsHelper}.
1551
   */
1552
  public final WindowsHelper getWindowsHelper() {
1553

1554
    if (this.windowsHelper == null) {
3✔
1555
      this.windowsHelper = createWindowsHelper();
4✔
1556
    }
1557
    return this.windowsHelper;
3✔
1558
  }
1559

1560
  /**
1561
   * @return the new {@link WindowsHelper} instance.
1562
   */
1563
  protected WindowsHelper createWindowsHelper() {
1564

1565
    return new WindowsHelperImpl(this);
×
1566
  }
1567

1568
  /**
1569
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1570
   */
1571
  public void reload() {
1572

1573
    this.variables = null;
3✔
1574
    this.customToolRepository = null;
3✔
1575
  }
1✔
1576

1577
  @Override
1578
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1579

1580
    assert (Files.isDirectory(installationPath));
6!
1581
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1582
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1583
  }
1✔
1584

1585
  /**
1586
   * @param home the IDE_HOME directory.
1587
   * @param workspace the name of the active workspace folder.
1588
   */
1589
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1590

1591
  }
1592

1593
  /**
1594
   * Returns the default git path on Windows. Required to be overwritten in tests.
1595
   *
1596
   * @return default path to git on Windows.
1597
   */
1598
  public String getDefaultWindowsGitPath() {
1599
    return DEFAULT_WINDOWS_GIT_PATH;
×
1600
  }
1601

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