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

devonfw / IDEasy / 18972198207

31 Oct 2025 12:14PM UTC coverage: 68.861% (-0.02%) from 68.876%
18972198207

Pull #1557

github

web-flow
Merge f7fa960a7 into 6cb666c38
Pull Request #1557: #1551: add NetworkStatus and improve status commandlet

3493 of 5553 branches covered (62.9%)

Branch coverage included in aggregate %.

9143 of 12797 relevant lines covered (71.45%)

3.14 hits per line

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

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

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

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

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

85
  private static final String LICENSE_URL = "https://github.com/devonfw/IDEasy/blob/main/documentation/LICENSE.adoc";
86
  public static final String BASH = "bash";
87

88
  private final IdeStartContextImpl startContext;
89

90
  private Path ideHome;
91

92
  private final Path ideRoot;
93

94
  private Path confPath;
95

96
  protected Path settingsPath;
97

98
  private Path settingsCommitIdPath;
99

100
  protected Path pluginsPath;
101

102
  private Path workspacePath;
103

104
  private String workspaceName;
105

106
  private Path cwd;
107

108
  private Path downloadPath;
109

110
  private Path userHome;
111

112
  private Path userHomeIde;
113

114
  private SystemPath path;
115

116
  private WindowsPathSyntax pathSyntax;
117

118
  private final SystemInfo systemInfo;
119

120
  private EnvironmentVariables variables;
121

122
  private final FileAccess fileAccess;
123

124
  protected CommandletManager commandletManager;
125

126
  protected ToolRepository defaultToolRepository;
127

128
  private CustomToolRepository customToolRepository;
129

130
  private MvnRepository mvnRepository;
131

132
  private NpmRepository npmRepository;
133

134
  private DirectoryMerger workspaceMerger;
135

136
  protected UrlMetadata urlMetadata;
137

138
  protected Path defaultExecutionDirectory;
139

140
  private StepImpl currentStep;
141

142
  private NetworkStatus networkStatus;
143

144
  protected IdeSystem system;
145

146
  private WindowsHelper windowsHelper;
147

148
  private final Map<String, String> privacyMap;
149

150
  /**
151
   * The constructor.
152
   *
153
   * @param startContext the {@link IdeLogger}.
154
   * @param workingDirectory the optional {@link Path} to current working directory.
155
   */
156
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
157

158
    super();
2✔
159
    this.startContext = startContext;
3✔
160
    this.startContext.setArgFormatter(this);
4✔
161
    this.privacyMap = new HashMap<>();
5✔
162
    this.systemInfo = SystemInfoImpl.INSTANCE;
3✔
163
    this.commandletManager = new CommandletManagerImpl(this);
6✔
164
    this.fileAccess = new FileAccessImpl(this);
6✔
165
    String userHomeProperty = getSystem().getProperty("user.home");
5✔
166
    if (userHomeProperty != null) {
2!
167
      this.userHome = Path.of(userHomeProperty);
×
168
    }
169
    String workspace = WORKSPACE_MAIN;
2✔
170
    if (workingDirectory == null) {
2!
171
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
172
    }
173
    workingDirectory = workingDirectory.toAbsolutePath();
3✔
174
    if (Files.isDirectory(workingDirectory)) {
5✔
175
      workingDirectory = this.fileAccess.toCanonicalPath(workingDirectory);
6✔
176
    } else {
177
      warning("Current working directory does not exist: {}", workingDirectory);
9✔
178
    }
179
    this.cwd = workingDirectory;
3✔
180
    // detect IDE_HOME and WORKSPACE
181
    Path currentDir = workingDirectory;
2✔
182
    String name1 = "";
2✔
183
    String name2 = "";
2✔
184
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
185
    while (currentDir != null) {
2✔
186
      trace("Looking for IDE_HOME in {}", currentDir);
9✔
187
      if (isIdeHome(currentDir)) {
4✔
188
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
189
          workspace = name2;
3✔
190
        }
191
        break;
192
      }
193
      name2 = name1;
2✔
194
      int nameCount = currentDir.getNameCount();
3✔
195
      if (nameCount >= 1) {
3✔
196
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
197
      }
198
      currentDir = currentDir.getParent();
3✔
199
      if ((ideRootPath != null) && (ideRootPath.equals(currentDir))) {
2!
200
        // prevent that during tests we traverse to the real IDE project of IDEasy developer
201
        currentDir = null;
×
202
      }
203
    }
1✔
204

205
    // detection completed, initializing variables
206
    this.ideRoot = findIdeRoot(currentDir);
5✔
207

208
    setCwd(workingDirectory, workspace, currentDir);
5✔
209

210
    if (this.ideRoot != null) {
3✔
211
      Path tempDownloadPath = getTempDownloadPath();
3✔
212
      if (Files.isDirectory(tempDownloadPath)) {
6✔
213
        // TODO delete all files older than 1 day here...
214
      } else {
215
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
216
      }
217
    }
218

219
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
220
  }
1✔
221

222
  /**
223
   * @return a new {@link MvnRepository}
224
   */
225
  protected MvnRepository createMvnRepository() {
226
    return new MvnRepository(this);
5✔
227
  }
228

229
  /**
230
   * @return a new {@link NpmRepository}
231
   */
232
  protected NpmRepository createNpmRepository() {
233
    return new NpmRepository(this);
5✔
234
  }
235

236
  private Path findIdeRoot(Path ideHomePath) {
237

238
    Path ideRootPath = null;
2✔
239
    if (ideHomePath != null) {
2✔
240
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
241
      ideRootPath = ideHomePath.getParent();
3✔
242
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
8!
243
        warning(
12✔
244
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
245
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
246
            ideRootPathFromEnv,
247
            ideHomePath.getFileName(), ideRootPath);
6✔
248
      }
249
    } else if (!isTest()) {
4!
250
      ideRootPath = getIdeRootPathFromEnv(true);
×
251
    }
252
    return ideRootPath;
2✔
253
  }
254

255
  /**
256
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
257
   */
258
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
259

260
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
261
    if (root != null) {
×
262
      Path rootPath = Path.of(root);
×
263
      if (Files.isDirectory(rootPath)) {
×
264
        Path absoluteRootPath = getFileAccess().toCanonicalPath(rootPath);
×
265
        if (withSanityCheck) {
×
266
          int nameCount = rootPath.getNameCount();
×
267
          int absoluteNameCount = absoluteRootPath.getNameCount();
×
268
          int delta = absoluteNameCount - nameCount;
×
269
          if (delta >= 0) {
×
270
            for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) {
×
271
              String rootName = rootPath.getName(nameIndex).toString();
×
272
              String absoluteRootName = absoluteRootPath.getName(nameIndex + delta).toString();
×
273
              if (!rootName.equals(absoluteRootName)) {
×
274
                warning("IDE_ROOT is set to {} but was expanded to absolute path {} and does not match for segment {} and {} - fix your IDEasy installation!",
×
275
                    rootPath, absoluteRootPath, rootName, absoluteRootName);
276
                break;
×
277
              }
278
            }
279
          } else {
280
            warning("IDE_ROOT is set to {} but was expanded to a shorter absolute path {}", rootPath,
×
281
                absoluteRootPath);
282
          }
283
        }
284
        return absoluteRootPath;
×
285
      } else if (withSanityCheck) {
×
286
        warning("IDE_ROOT is set to {} that is not an existing directory - fix your IDEasy installation!", rootPath);
×
287
      }
288
    }
289
    return null;
×
290
  }
291

292
  @Override
293
  public void setCwd(Path userDir, String workspace, Path ideHome) {
294

295
    this.cwd = userDir;
3✔
296
    this.workspaceName = workspace;
3✔
297
    this.ideHome = ideHome;
3✔
298
    if (ideHome == null) {
2✔
299
      this.workspacePath = null;
3✔
300
      this.confPath = null;
3✔
301
      this.settingsPath = null;
3✔
302
      this.pluginsPath = null;
4✔
303
    } else {
304
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
305
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
306
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
307
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
308
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
309
    }
310
    if (isTest()) {
3!
311
      // only for testing...
312
      if (this.ideHome == null) {
3✔
313
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
314
      } else {
315
        this.userHome = this.ideHome.resolve("home");
6✔
316
      }
317
    }
318
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
319
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
320
    resetPrivacyMap();
2✔
321
    this.path = computeSystemPath();
4✔
322
  }
1✔
323

324
  private String getMessageIdeHomeFound() {
325

326
    String wks = this.workspaceName;
3✔
327
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
328
      wks = "*".repeat(wks.length());
×
329
    }
330
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
331
  }
332

333
  private String getMessageNotInsideIdeProject() {
334

335
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
336
  }
337

338
  private String getMessageIdeRootNotFound() {
339

340
    String root = getSystem().getEnv("IDE_ROOT");
5✔
341
    if (root == null) {
2!
342
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
343
    } else {
344
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
345
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
346
    }
347
  }
348

349
  /**
350
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
351
   */
352
  public boolean isTest() {
353

354
    return false;
×
355
  }
356

357
  protected SystemPath computeSystemPath() {
358

359
    return new SystemPath(this);
×
360
  }
361

362
  private boolean isIdeHome(Path dir) {
363

364
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
365
      return false;
2✔
366
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
367
      return false;
×
368
    }
369
    return true;
2✔
370
  }
371

372
  private EnvironmentVariables createVariables() {
373

374
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
375
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
376
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
377
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
378
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
379
    return conf.resolved();
3✔
380
  }
381

382
  protected AbstractEnvironmentVariables createSystemVariables() {
383

384
    return EnvironmentVariables.ofSystem(this);
3✔
385
  }
386

387
  @Override
388
  public SystemInfo getSystemInfo() {
389

390
    return this.systemInfo;
3✔
391
  }
392

393
  @Override
394
  public FileAccess getFileAccess() {
395

396
    return this.fileAccess;
3✔
397
  }
398

399
  @Override
400
  public CommandletManager getCommandletManager() {
401

402
    return this.commandletManager;
3✔
403
  }
404

405
  @Override
406
  public ToolRepository getDefaultToolRepository() {
407

408
    return this.defaultToolRepository;
3✔
409
  }
410

411
  @Override
412
  public MvnRepository getMvnRepository() {
413
    if (this.mvnRepository == null) {
3✔
414
      this.mvnRepository = createMvnRepository();
4✔
415
    }
416
    return this.mvnRepository;
3✔
417
  }
418

419
  @Override
420
  public NpmRepository getNpmRepository() {
421
    if (this.npmRepository == null) {
3✔
422
      this.npmRepository = createNpmRepository();
4✔
423
    }
424
    return this.npmRepository;
3✔
425
  }
426

427
  @Override
428
  public CustomToolRepository getCustomToolRepository() {
429

430
    if (this.customToolRepository == null) {
3!
431
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
432
    }
433
    return this.customToolRepository;
3✔
434
  }
435

436
  @Override
437
  public Path getIdeHome() {
438

439
    return this.ideHome;
3✔
440
  }
441

442
  @Override
443
  public String getProjectName() {
444

445
    if (this.ideHome != null) {
3!
446
      return this.ideHome.getFileName().toString();
5✔
447
    }
448
    return "";
×
449
  }
450

451
  @Override
452
  public VersionIdentifier getProjectVersion() {
453

454
    if (this.ideHome != null) {
3!
455
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
456
      if (Files.exists(versionFile)) {
5✔
457
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
458
        return VersionIdentifier.of(version);
3✔
459
      }
460
    }
461
    return IdeMigrator.START_VERSION;
2✔
462
  }
463

464
  @Override
465
  public void setProjectVersion(VersionIdentifier version) {
466

467
    if (this.ideHome == null) {
3!
468
      throw new IllegalStateException("IDE_HOME not available!");
×
469
    }
470
    Objects.requireNonNull(version);
3✔
471
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
472
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
473
  }
1✔
474

475
  @Override
476
  public Path getIdeRoot() {
477

478
    return this.ideRoot;
3✔
479
  }
480

481
  @Override
482
  public Path getIdePath() {
483

484
    Path myIdeRoot = getIdeRoot();
3✔
485
    if (myIdeRoot == null) {
2!
486
      return null;
×
487
    }
488
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
489
  }
490

491
  @Override
492
  public Path getCwd() {
493

494
    return this.cwd;
3✔
495
  }
496

497
  @Override
498
  public Path getTempPath() {
499

500
    Path idePath = getIdePath();
3✔
501
    if (idePath == null) {
2!
502
      return null;
×
503
    }
504
    return idePath.resolve("tmp");
4✔
505
  }
506

507
  @Override
508
  public Path getTempDownloadPath() {
509

510
    Path tmp = getTempPath();
3✔
511
    if (tmp == null) {
2!
512
      return null;
×
513
    }
514
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
515
  }
516

517
  @Override
518
  public Path getUserHome() {
519

520
    return this.userHome;
3✔
521
  }
522

523
  /**
524
   * This method should only be used for tests to mock user home.
525
   *
526
   * @param userHome the new value of {@link #getUserHome()}.
527
   */
528
  protected void setUserHome(Path userHome) {
529

530
    this.userHome = userHome;
3✔
531
    resetPrivacyMap();
2✔
532
  }
1✔
533

534
  @Override
535
  public Path getUserHomeIde() {
536

537
    return this.userHomeIde;
3✔
538
  }
539

540
  @Override
541
  public Path getSettingsPath() {
542

543
    return this.settingsPath;
3✔
544
  }
545

546
  @Override
547
  public Path getSettingsGitRepository() {
548

549
    Path settingsPath = getSettingsPath();
3✔
550
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
551
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
552
      error("Settings repository exists but is not a git repository.");
3✔
553
      return null;
2✔
554
    }
555
    return settingsPath;
2✔
556
  }
557

558
  @Override
559
  public boolean isSettingsRepositorySymlinkOrJunction() {
560

561
    Path settingsPath = getSettingsPath();
3✔
562
    if (settingsPath == null) {
2!
563
      return false;
×
564
    }
565
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
566
  }
567

568
  @Override
569
  public Path getSettingsCommitIdPath() {
570

571
    return this.settingsCommitIdPath;
3✔
572
  }
573

574
  @Override
575
  public Path getConfPath() {
576

577
    return this.confPath;
3✔
578
  }
579

580
  @Override
581
  public Path getSoftwarePath() {
582

583
    if (this.ideHome == null) {
3✔
584
      return null;
2✔
585
    }
586
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
587
  }
588

589
  @Override
590
  public Path getSoftwareExtraPath() {
591

592
    Path softwarePath = getSoftwarePath();
3✔
593
    if (softwarePath == null) {
2!
594
      return null;
×
595
    }
596
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
597
  }
598

599
  @Override
600
  public Path getSoftwareRepositoryPath() {
601

602
    Path idePath = getIdePath();
3✔
603
    if (idePath == null) {
2!
604
      return null;
×
605
    }
606
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
607
  }
608

609
  @Override
610
  public Path getPluginsPath() {
611

612
    return this.pluginsPath;
3✔
613
  }
614

615
  @Override
616
  public String getWorkspaceName() {
617

618
    return this.workspaceName;
3✔
619
  }
620

621
  @Override
622
  public Path getWorkspacePath() {
623

624
    return this.workspacePath;
3✔
625
  }
626

627
  @Override
628
  public Path getDownloadPath() {
629

630
    return this.downloadPath;
3✔
631
  }
632

633
  @Override
634
  public Path getUrlsPath() {
635

636
    Path idePath = getIdePath();
3✔
637
    if (idePath == null) {
2!
638
      return null;
×
639
    }
640
    return idePath.resolve(FOLDER_URLS);
4✔
641
  }
642

643
  @Override
644
  public Path getToolRepositoryPath() {
645

646
    Path idePath = getIdePath();
3✔
647
    if (idePath == null) {
2!
648
      return null;
×
649
    }
650
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
651
  }
652

653
  @Override
654
  public SystemPath getPath() {
655

656
    return this.path;
3✔
657
  }
658

659
  @Override
660
  public EnvironmentVariables getVariables() {
661

662
    if (this.variables == null) {
3✔
663
      this.variables = createVariables();
4✔
664
    }
665
    return this.variables;
3✔
666
  }
667

668
  @Override
669
  public UrlMetadata getUrls() {
670

671
    if (this.urlMetadata == null) {
3✔
672
      if (!isTest()) {
3!
673
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
674
      }
675
      this.urlMetadata = new UrlMetadata(this);
6✔
676
    }
677
    return this.urlMetadata;
3✔
678
  }
679

680
  @Override
681
  public boolean isQuietMode() {
682

683
    return this.startContext.isQuietMode();
4✔
684
  }
685

686
  @Override
687
  public boolean isBatchMode() {
688

689
    return this.startContext.isBatchMode();
4✔
690
  }
691

692
  @Override
693
  public boolean isForceMode() {
694

695
    return this.startContext.isForceMode();
4✔
696
  }
697

698
  @Override
699
  public boolean isForcePull() {
700

701
    return this.startContext.isForcePull();
4✔
702
  }
703

704
  @Override
705
  public boolean isForcePlugins() {
706

707
    return this.startContext.isForcePlugins();
4✔
708
  }
709

710
  @Override
711
  public boolean isForceRepositories() {
712

713
    return this.startContext.isForceRepositories();
4✔
714
  }
715

716
  @Override
717
  public boolean isOfflineMode() {
718

719
    return this.startContext.isOfflineMode();
4✔
720
  }
721

722
  @Override
723
  public boolean isPrivacyMode() {
724
    return this.startContext.isPrivacyMode();
4✔
725
  }
726

727
  @Override
728
  public boolean isSkipUpdatesMode() {
729

730
    return this.startContext.isSkipUpdatesMode();
4✔
731
  }
732

733
  @Override
734
  public boolean isNoColorsMode() {
735

736
    return this.startContext.isNoColorsMode();
×
737
  }
738

739
  @Override
740
  public NetworkStatus getNetworkStatus() {
741

742
    if (this.networkStatus == null) {
×
743
      this.networkStatus = new NetworkStatusImpl(this);
×
744
    }
745
    return this.networkStatus;
×
746
  }
747

748
  @Override
749
  public Locale getLocale() {
750

751
    Locale locale = this.startContext.getLocale();
4✔
752
    if (locale == null) {
2✔
753
      locale = Locale.getDefault();
2✔
754
    }
755
    return locale;
2✔
756
  }
757

758
  @Override
759
  public DirectoryMerger getWorkspaceMerger() {
760

761
    if (this.workspaceMerger == null) {
3✔
762
      this.workspaceMerger = new DirectoryMerger(this);
6✔
763
    }
764
    return this.workspaceMerger;
3✔
765
  }
766

767
  /**
768
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
769
   */
770
  @Override
771
  public Path getDefaultExecutionDirectory() {
772

773
    return this.defaultExecutionDirectory;
×
774
  }
775

776
  /**
777
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
778
   */
779
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
780

781
    if (defaultExecutionDirectory != null) {
×
782
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
783
    }
784
  }
×
785

786
  @Override
787
  public GitContext getGitContext() {
788

789
    return new GitContextImpl(this);
×
790
  }
791

792
  @Override
793
  public ProcessContext newProcess() {
794

795
    ProcessContext processContext = createProcessContext();
3✔
796
    if (this.defaultExecutionDirectory != null) {
3!
797
      processContext.directory(this.defaultExecutionDirectory);
×
798
    }
799
    return processContext;
2✔
800
  }
801

802
  @Override
803
  public IdeSystem getSystem() {
804

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

811
  /**
812
   * @return a new instance of {@link ProcessContext}.
813
   * @see #newProcess()
814
   */
815
  protected ProcessContext createProcessContext() {
816

817
    return new ProcessContextImpl(this);
5✔
818
  }
819

820
  @Override
821
  public IdeSubLogger level(IdeLogLevel level) {
822

823
    return this.startContext.level(level);
5✔
824
  }
825

826
  @Override
827
  public void logIdeHomeAndRootStatus() {
828
    if (this.ideRoot != null) {
3!
829
      success("IDE_ROOT is set to {}", this.ideRoot);
×
830
    }
831
    if (this.ideHome == null) {
3✔
832
      warning(getMessageNotInsideIdeProject());
5✔
833
    } else {
834
      success("IDE_HOME is set to {}", this.ideHome);
10✔
835
    }
836
  }
1✔
837

838
  @Override
839
  public String formatArgument(Object argument) {
840

841
    if (argument == null) {
2✔
842
      return null;
2✔
843
    }
844
    String result = argument.toString();
3✔
845
    if (isPrivacyMode()) {
3✔
846
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
847
        initializePrivacyMap(this.userHome, "~");
×
848
        String projectName = getProjectName();
×
849
        if (!projectName.isEmpty()) {
×
850
          this.privacyMap.put(projectName, "project");
×
851
        }
852
      }
853
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
854
        result = result.replace(entry.getKey(), entry.getValue());
×
855
      }
×
856
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
857
    }
858
    return result;
2✔
859
  }
860

861
  /**
862
   * @param path the sensitive {@link Path} to
863
   * @param replacement the replacement to mask the {@link Path} in log output.
864
   */
865
  protected void initializePrivacyMap(Path path, String replacement) {
866

867
    if (path == null) {
×
868
      return;
×
869
    }
870
    if (this.systemInfo.isWindows()) {
×
871
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
872
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
873
    } else {
874
      this.privacyMap.put(path.toString(), replacement);
×
875
    }
876
  }
×
877

878
  /**
879
   * Resets the privacy map in case fundamental values have changed.
880
   */
881
  private void resetPrivacyMap() {
882

883
    this.privacyMap.clear();
3✔
884
  }
1✔
885

886

887
  @Override
888
  public String askForInput(String message, String defaultValue) {
889

890
    while (true) {
891
      if (!message.isBlank()) {
3!
892
        interaction(message);
3✔
893
      }
894
      if (isBatchMode()) {
3!
895
        if (isForceMode()) {
×
896
          return defaultValue;
×
897
        } else {
898
          throw new CliAbortException();
×
899
        }
900
      }
901
      String input = readLine().trim();
4✔
902
      if (!input.isEmpty()) {
3!
903
        return input;
2✔
904
      } else {
905
        if (defaultValue != null) {
×
906
          return defaultValue;
×
907
        }
908
      }
909
    }
×
910
  }
911

912
  @SuppressWarnings("unchecked")
913
  @Override
914
  public <O> O question(O[] options, String question, Object... args) {
915

916
    assert (options.length >= 2);
5!
917
    interaction(question, args);
4✔
918
    return displayOptionsAndGetAnswer(options);
4✔
919
  }
920

921
  private <O> O displayOptionsAndGetAnswer(O[] options) {
922
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
923
    int i = 0;
2✔
924
    for (O option : options) {
16✔
925
      i++;
1✔
926
      String key = "" + option;
4✔
927
      addMapping(mapping, key, option);
4✔
928
      String numericKey = Integer.toString(i);
3✔
929
      if (numericKey.equals(key)) {
4!
930
        trace("Options should not be numeric: " + key);
×
931
      } else {
932
        addMapping(mapping, numericKey, option);
4✔
933
      }
934
      interaction("Option " + numericKey + ": " + key);
5✔
935
    }
936
    O option = null;
2✔
937
    if (isBatchMode()) {
3!
938
      if (isForceMode()) {
×
939
        option = options[0];
×
940
        interaction("" + option);
×
941
      }
942
    } else {
943
      while (option == null) {
2✔
944
        String answer = readLine();
3✔
945
        option = mapping.get(answer);
4✔
946
        if (option == null) {
2!
947
          warning("Invalid answer: '" + answer + "' - please try again.");
×
948
        }
949
      }
1✔
950
    }
951
    return option;
2✔
952
  }
953

954
  /**
955
   * @return the input from the end-user (e.g. read from the console).
956
   */
957
  protected abstract String readLine();
958

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

961
    O duplicate = mapping.put(key, option);
5✔
962
    if (duplicate != null) {
2!
963
      throw new IllegalArgumentException("Duplicated option " + key);
×
964
    }
965
  }
1✔
966

967
  @Override
968
  public Step getCurrentStep() {
969

970
    return this.currentStep;
×
971
  }
972

973
  @Override
974
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
975

976
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
977
    return this.currentStep;
3✔
978
  }
979

980
  /**
981
   * Internal method to end the running {@link Step}.
982
   *
983
   * @param step the current {@link Step} to end.
984
   */
985
  public void endStep(StepImpl step) {
986

987
    if (step == this.currentStep) {
4!
988
      this.currentStep = this.currentStep.getParent();
6✔
989
    } else {
990
      String currentStepName = "null";
×
991
      if (this.currentStep != null) {
×
992
        currentStepName = this.currentStep.getName();
×
993
      }
994
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
995
    }
996
  }
1✔
997

998
  /**
999
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1000
   *
1001
   * @param arguments the {@link CliArgument}.
1002
   * @return the return code of the execution.
1003
   */
1004
  public int run(CliArguments arguments) {
1005

1006
    CliArgument current = arguments.current();
3✔
1007
    assert (this.currentStep == null);
4!
1008
    boolean supressStepSuccess = false;
2✔
1009
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1010
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1011
    Commandlet cmd = null;
2✔
1012
    ValidationResult result = null;
2✔
1013
    try {
1014
      while (commandletIterator.hasNext()) {
3✔
1015
        cmd = commandletIterator.next();
4✔
1016
        result = applyAndRun(arguments.copy(), cmd);
6✔
1017
        if (result.isValid()) {
3!
1018
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1019
          step.success();
2✔
1020
          return ProcessResult.SUCCESS;
4✔
1021
        }
1022
      }
1023
      this.startContext.activateLogging();
3✔
1024
      verifyIdeMinVersion(false);
3✔
1025
      if (result != null) {
2!
1026
        error(result.getErrorMessage());
×
1027
      }
1028
      step.error("Invalid arguments: {}", current.getArgs());
10✔
1029
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
1030
      if (cmd != null) {
2!
1031
        help.commandlet.setValue(cmd);
×
1032
      }
1033
      help.run();
2✔
1034
      return 1;
4✔
1035
    } catch (Throwable t) {
1✔
1036
      this.startContext.activateLogging();
3✔
1037
      step.error(t, true);
4✔
1038
      throw t;
2✔
1039
    } finally {
1040
      step.close();
2✔
1041
      assert (this.currentStep == null);
4!
1042
      step.logSummary(supressStepSuccess);
3✔
1043
    }
1044
  }
1045

1046
  @Override
1047
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1048

1049
    this.startContext.deactivateLogging(threshold);
4✔
1050
    lambda.run();
2✔
1051
    this.startContext.activateLogging();
3✔
1052
  }
1✔
1053

1054
  /**
1055
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1056
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1057
   *     {@link Commandlet} did not match and we have to try a different candidate).
1058
   */
1059
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1060

1061
    IdeLogLevel previousLogLevel = null;
2✔
1062
    cmd.reset();
2✔
1063
    ValidationResult result = apply(arguments, cmd);
5✔
1064
    if (result.isValid()) {
3!
1065
      result = cmd.validate();
3✔
1066
    }
1067
    if (result.isValid()) {
3!
1068
      debug("Running commandlet {}", cmd);
9✔
1069
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1070
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1071
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1072
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1073
      }
1074
      try {
1075
        if (cmd.isProcessableOutput()) {
3!
1076
          if (!debug().isEnabled()) {
×
1077
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1078
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
1079
          }
1080
          this.startContext.activateLogging();
×
1081
        } else {
1082
          this.startContext.activateLogging();
3✔
1083
          if (cmd.isIdeHomeRequired()) {
3!
1084
            debug(getMessageIdeHomeFound());
4✔
1085
          }
1086
          Path settingsRepository = getSettingsGitRepository();
3✔
1087
          if (settingsRepository != null) {
2!
1088
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1089
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1090
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1091
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1092
                interaction(
×
1093
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
1094

1095
              } else {
1096
                interaction(
×
1097
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
1098
              }
1099
            }
1100
          }
1101
        }
1102
        boolean success = ensureLicenseAgreement(cmd);
4✔
1103
        if (!success) {
2!
1104
          return ValidationResultValid.get();
×
1105
        }
1106
        cmd.run();
2✔
1107
      } finally {
1108
        if (previousLogLevel != null) {
2!
1109
          this.startContext.setLogLevel(previousLogLevel);
×
1110
        }
1111
      }
1✔
1112
    } else {
1113
      trace("Commandlet did not match");
×
1114
    }
1115
    return result;
2✔
1116
  }
1117

1118
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1119

1120
    if (isTest()) {
3!
1121
      return true; // ignore for tests
2✔
1122
    }
1123
    getFileAccess().mkdirs(this.userHomeIde);
×
1124
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1125
    if (Files.isRegularFile(licenseAgreement)) {
×
1126
      return true; // success, license already accepted
×
1127
    }
1128
    if (cmd instanceof EnvironmentCommandlet) {
×
1129
      // 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
1130
      // 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
1131
      // printing anything anymore in such case.
1132
      return false;
×
1133
    }
1134
    boolean logLevelInfoDisabled = !this.startContext.info().isEnabled();
×
1135
    if (logLevelInfoDisabled) {
×
1136
      this.startContext.setLogLevel(IdeLogLevel.INFO, true);
×
1137
    }
1138
    boolean logLevelInteractionDisabled = !this.startContext.interaction().isEnabled();
×
1139
    if (logLevelInteractionDisabled) {
×
1140
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, true);
×
1141
    }
1142
    StringBuilder sb = new StringBuilder(1180);
×
1143
    sb.append(LOGO).append("""
×
1144
        Welcome to IDEasy!
1145
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1146
        It supports automatic download and installation of arbitrary 3rd party tools.
1147
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1148
        But if explicitly configured, also commercial software that requires an additional license may be used.
1149
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1150
        You are solely responsible for all risks implied by using this software.
1151
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1152
        You will be able to find it online under the following URL:
1153
        """).append(LICENSE_URL);
×
1154
    if (this.ideRoot != null) {
×
1155
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1156
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1157
    }
1158
    info(sb.toString());
×
1159
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1160

1161
    sb.setLength(0);
×
1162
    LocalDateTime now = LocalDateTime.now();
×
1163
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1164
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1165
    try {
1166
      Files.writeString(licenseAgreement, sb);
×
1167
    } catch (Exception e) {
×
1168
      throw new RuntimeException("Failed to save license agreement!", e);
×
1169
    }
×
1170
    if (logLevelInfoDisabled) {
×
1171
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
1172
    }
1173
    if (logLevelInteractionDisabled) {
×
1174
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
1175
    }
1176
    return true;
×
1177
  }
1178

1179
  @Override
1180
  public void verifyIdeMinVersion(boolean throwException) {
1181
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1182
    if (minVersion == null) {
2✔
1183
      return;
1✔
1184
    }
1185
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1186
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1187
          + "However, this is too old as your project requires at latest version %s\n"
1188
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1189
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1190
      if (throwException) {
2✔
1191
        throw new CliException(message);
5✔
1192
      } else {
1193
        warning(message);
3✔
1194
      }
1195
    }
1196
  }
1✔
1197

1198
  /**
1199
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1200
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1201
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1202
   */
1203
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1204

1205
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1206
    if (arguments.current().isStart()) {
4✔
1207
      arguments.next();
3✔
1208
    }
1209
    if (includeContextOptions) {
2✔
1210
      ContextCommandlet cc = new ContextCommandlet();
4✔
1211
      for (Property<?> property : cc.getProperties()) {
11✔
1212
        assert (property.isOption());
4!
1213
        property.apply(arguments, this, cc, collector);
7✔
1214
      }
1✔
1215
    }
1216
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1217
    CliArgument current = arguments.current();
3✔
1218
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1219
      collector.add(current.get(), null, null, null);
7✔
1220
    }
1221
    arguments.next();
3✔
1222
    while (commandletIterator.hasNext()) {
3✔
1223
      Commandlet cmd = commandletIterator.next();
4✔
1224
      if (!arguments.current().isEnd()) {
4✔
1225
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1226
      }
1227
    }
1✔
1228
    return collector.getSortedCandidates();
3✔
1229
  }
1230

1231
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1232

1233
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
1234
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1235
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1236
    List<Property<?>> properties = cmd.getProperties();
3✔
1237
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1238
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1239
    for (Property<?> property : properties) {
10✔
1240
      if (property.isOption()) {
3✔
1241
        optionProperties.add(property);
4✔
1242
      }
1243
    }
1✔
1244
    CliArgument currentArgument = arguments.current();
3✔
1245
    while (!currentArgument.isEnd()) {
3✔
1246
      trace("Trying to match argument '{}'", currentArgument);
9✔
1247
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1248
        if (currentArgument.isCompletion()) {
3✔
1249
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1250
          while (optionIterator.hasNext()) {
3✔
1251
            Property<?> option = optionIterator.next();
4✔
1252
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1253
            if (success) {
2✔
1254
              optionIterator.remove();
2✔
1255
              arguments.next();
3✔
1256
            }
1257
          }
1✔
1258
        } else {
1✔
1259
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1260
          if (option != null) {
2✔
1261
            arguments.next();
3✔
1262
            boolean removed = optionProperties.remove(option);
4✔
1263
            if (!removed) {
2!
1264
              option = null;
×
1265
            }
1266
          }
1267
          if (option == null) {
2✔
1268
            trace("No such option was found.");
3✔
1269
            return;
1✔
1270
          }
1271
        }
1✔
1272
      } else {
1273
        if (valueIterator.hasNext()) {
3✔
1274
          Property<?> valueProperty = valueIterator.next();
4✔
1275
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1276
          if (!success) {
2✔
1277
            trace("Completion cannot match any further.");
3✔
1278
            return;
1✔
1279
          }
1280
        } else {
1✔
1281
          trace("No value left for completion.");
3✔
1282
          return;
1✔
1283
        }
1284
      }
1285
      currentArgument = arguments.current();
4✔
1286
    }
1287
  }
1✔
1288

1289
  /**
1290
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1291
   *     {@link CliArguments#copy() copy} as needed.
1292
   * @param cmd the potential {@link Commandlet} to match.
1293
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1294
   */
1295
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1296

1297
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1298
    CliArgument currentArgument = arguments.current();
3✔
1299
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1300
    Property<?> property = null;
2✔
1301
    if (propertyIterator.hasNext()) {
3!
1302
      property = propertyIterator.next();
4✔
1303
    }
1304
    while (!currentArgument.isEnd()) {
3✔
1305
      trace("Trying to match argument '{}'", currentArgument);
9✔
1306
      Property<?> currentProperty = property;
2✔
1307
      if (!arguments.isEndOptions()) {
3!
1308
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1309
        if (option != null) {
2!
1310
          currentProperty = option;
×
1311
        }
1312
      }
1313
      if (currentProperty == null) {
2!
1314
        trace("No option or next value found");
×
1315
        ValidationState state = new ValidationState(null);
×
1316
        state.addErrorMessage("No matching property found");
×
1317
        return state;
×
1318
      }
1319
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1320
      if (currentProperty == property) {
3!
1321
        if (!property.isMultiValued()) {
3✔
1322
          if (propertyIterator.hasNext()) {
3✔
1323
            property = propertyIterator.next();
5✔
1324
          } else {
1325
            property = null;
2✔
1326
          }
1327
        }
1328
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1329
          arguments.stopSplitShortOptions();
2✔
1330
        }
1331
      }
1332
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1333
      if (!matches) {
2!
1334
        ValidationState state = new ValidationState(null);
×
1335
        state.addErrorMessage("No matching property found");
×
1336
        return state;
×
1337
      }
1338
      currentArgument = arguments.current();
3✔
1339
    }
1✔
1340
    return ValidationResultValid.get();
2✔
1341
  }
1342

1343
  @Override
1344
  public String findBash() {
1345

1346
    String bash = BASH;
2✔
1347
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1348
      bash = findBashOnWindows();
×
1349
      if (bash == null) {
×
1350
        String variable = IdeVariables.BASH_PATH.getName();
×
1351
        bash = getVariables().get(variable);
×
1352
        if (bash == null) {
×
1353
          trace("Bash not found. Trying to search on system PATH.");
×
1354
          variable = IdeVariables.PATH.getName();
×
1355
          Path plainBash = Path.of(BASH);
×
1356
          Path bashPath = getPath().findBinary(plainBash);
×
1357
          bash = bashPath.toAbsolutePath().toString();
×
1358
          if (bash.contains("AppData\\Local\\Microsoft\\WindowsApps")) {
×
1359
            warning("Only found windows fake bash that is not usable!");
×
1360
            bash = null;
×
1361
          }
1362
        }
1363
        if (bash == null) {
×
1364
          info("Could not find bash in Windows registry, using bash from {} as fallback: {}", variable, bash);
×
1365
        }
1366
      }
1367
    }
1368
    return bash;
2✔
1369
  }
1370

1371
  private String findBashOnWindows() {
1372

1373
    // Check if Git Bash exists in the default location
1374
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1375
    if (Files.exists(defaultPath)) {
×
1376
      return defaultPath.toString();
×
1377
    }
1378

1379
    // If not found in the default location, try the registry query
1380
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1381
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1382
    String regQueryResult;
1383
    for (String bashVariant : bashVariants) {
×
1384
      trace("Trying to find bash variant: {}", bashVariant);
×
1385
      for (String registryKey : registryKeys) {
×
1386
        trace("Trying to find bash from registry key: {}", registryKey);
×
1387
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1388
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1389

1390
        try {
1391
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1392
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1393
            StringBuilder output = new StringBuilder();
×
1394
            String line;
1395

1396
            while ((line = reader.readLine()) != null) {
×
1397
              output.append(line);
×
1398
            }
1399

1400
            int exitCode = process.waitFor();
×
1401
            if (exitCode != 0) {
×
1402
              warning("Query to windows registry for finding bash failed with exit code {}", exitCode);
×
1403
              return null;
×
1404
            }
1405

1406
            regQueryResult = output.toString();
×
1407
            trace("Result from windows registry was: {}", regQueryResult);
×
1408
            int index = regQueryResult.indexOf("REG_SZ");
×
1409
            if (index != -1) {
×
1410
              String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1411
              String bashPath = path + "\\bin\\bash.exe";
×
1412
              debug("Found bash at: {}", bashPath);
×
1413
              return bashPath;
×
1414
            }
1415
          }
×
1416
        } catch (Exception e) {
×
1417
          error(e, "Query to windows registry for finding bash failed!");
×
1418
          return null;
×
1419
        }
×
1420
      }
1421
    }
1422
    // no bash found
1423
    return null;
×
1424
  }
1425

1426
  @Override
1427
  public WindowsPathSyntax getPathSyntax() {
1428

1429
    return this.pathSyntax;
3✔
1430
  }
1431

1432
  /**
1433
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1434
   */
1435
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1436

1437
    this.pathSyntax = pathSyntax;
3✔
1438
  }
1✔
1439

1440
  /**
1441
   * @return the {@link IdeStartContextImpl}.
1442
   */
1443
  public IdeStartContextImpl getStartContext() {
1444

1445
    return startContext;
3✔
1446
  }
1447

1448
  /**
1449
   * @return the {@link WindowsHelper}.
1450
   */
1451
  public final WindowsHelper getWindowsHelper() {
1452

1453
    if (this.windowsHelper == null) {
3✔
1454
      this.windowsHelper = createWindowsHelper();
4✔
1455
    }
1456
    return this.windowsHelper;
3✔
1457
  }
1458

1459
  /**
1460
   * @return the new {@link WindowsHelper} instance.
1461
   */
1462
  protected WindowsHelper createWindowsHelper() {
1463

1464
    return new WindowsHelperImpl(this);
×
1465
  }
1466

1467
  /**
1468
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1469
   */
1470
  public void reload() {
1471

1472
    this.variables = null;
3✔
1473
    this.customToolRepository = null;
3✔
1474
  }
1✔
1475

1476
  @Override
1477
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1478

1479
    assert (Files.isDirectory(installationPath));
6!
1480
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1481
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1482
  }
1✔
1483

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