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

devonfw / IDEasy / 15822927784

23 Jun 2025 11:25AM UTC coverage: 67.978% (+0.2%) from 67.793%
15822927784

Pull #1370

github

web-flow
Merge 1d04bd1b1 into f0c645132
Pull Request #1370: #1369: Omit username from StatusCommandlet

3192 of 5104 branches covered (62.54%)

Branch coverage included in aggregate %.

8180 of 11625 relevant lines covered (70.37%)

3.09 hits per line

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

60.51
cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java
1
package com.devonfw.tools.ide.context;
2

3
import static com.devonfw.tools.ide.variable.IdeVariables.IDE_MIN_VERSION;
4

5
import java.io.BufferedReader;
6
import java.io.InputStreamReader;
7
import java.net.URL;
8
import java.net.URLConnection;
9
import java.nio.file.Files;
10
import java.nio.file.Path;
11
import java.nio.file.Paths;
12
import java.time.LocalDateTime;
13
import java.util.ArrayList;
14
import java.util.HashMap;
15
import java.util.Iterator;
16
import java.util.List;
17
import java.util.Locale;
18
import java.util.Map;
19
import java.util.Objects;
20

21
import com.devonfw.tools.ide.cli.CliAbortException;
22
import com.devonfw.tools.ide.cli.CliArgument;
23
import com.devonfw.tools.ide.cli.CliArguments;
24
import com.devonfw.tools.ide.cli.CliException;
25
import com.devonfw.tools.ide.commandlet.Commandlet;
26
import com.devonfw.tools.ide.commandlet.CommandletManager;
27
import com.devonfw.tools.ide.commandlet.CommandletManagerImpl;
28
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
29
import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet;
30
import com.devonfw.tools.ide.commandlet.HelpCommandlet;
31
import com.devonfw.tools.ide.common.SystemPath;
32
import com.devonfw.tools.ide.completion.CompletionCandidate;
33
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
34
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault;
35
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
36
import com.devonfw.tools.ide.environment.EnvironmentVariables;
37
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
38
import com.devonfw.tools.ide.environment.IdeSystem;
39
import com.devonfw.tools.ide.environment.IdeSystemImpl;
40
import com.devonfw.tools.ide.git.GitContext;
41
import com.devonfw.tools.ide.git.GitContextImpl;
42
import com.devonfw.tools.ide.git.GitUrl;
43
import com.devonfw.tools.ide.io.FileAccess;
44
import com.devonfw.tools.ide.io.FileAccessImpl;
45
import com.devonfw.tools.ide.log.IdeLogLevel;
46
import com.devonfw.tools.ide.log.IdeLogger;
47
import com.devonfw.tools.ide.log.IdeSubLogger;
48
import com.devonfw.tools.ide.merge.DirectoryMerger;
49
import com.devonfw.tools.ide.migration.IdeMigrator;
50
import com.devonfw.tools.ide.network.NetworkProxy;
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.MavenRepository;
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.validation.ValidationResult;
70
import com.devonfw.tools.ide.validation.ValidationResultValid;
71
import com.devonfw.tools.ide.validation.ValidationState;
72
import com.devonfw.tools.ide.variable.IdeVariables;
73
import com.devonfw.tools.ide.version.IdeVersion;
74
import com.devonfw.tools.ide.version.VersionIdentifier;
75

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

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

83
  private static final String LICENSE_URL = "https://github.com/devonfw/IDEasy/blob/main/documentation/LICENSE.adoc";
84
  private static final String IDE_HOME_PLACEHOLDER = "$IDE_HOME";
85
  private static final String IDE_ROOT_PLACEHOLDER = "$IDE_ROOT";
86
  private static final String USER_HOME_PLACEHOLDER = "~";
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
  protected 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 MavenRepository mavenRepository;
131

132
  private DirectoryMerger workspaceMerger;
133

134
  protected UrlMetadata urlMetadata;
135

136
  protected Path defaultExecutionDirectory;
137

138
  private StepImpl currentStep;
139

140
  protected Boolean online;
141

142
  protected IdeSystem system;
143

144
  private NetworkProxy networkProxy;
145

146
  private WindowsHelper windowsHelper;
147

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

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

197
    // detection completed, initializing variables
198
    this.ideRoot = findIdeRoot(currentDir);
5✔
199

200
    setCwd(workingDirectory, workspace, currentDir);
5✔
201

202
    if (this.ideRoot != null) {
3✔
203
      Path tempDownloadPath = getTempDownloadPath();
3✔
204
      if (Files.isDirectory(tempDownloadPath)) {
6✔
205
        // TODO delete all files older than 1 day here...
206
      } else {
207
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
208
      }
209
    }
210

211
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
212
    this.mavenRepository = new MavenRepository(this);
6✔
213
  }
1✔
214

215
  private Path findIdeRoot(Path ideHomePath) {
216

217
    Path ideRootPath = null;
2✔
218
    if (ideHomePath != null) {
2✔
219
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
220
      ideRootPath = ideHomePath.getParent();
3✔
221
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
8!
222
        warning(
9✔
223
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
224
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
225
            formatLocationPathForDisplay(ideRootPathFromEnv),
5✔
226
            ideHomePath.getFileName(), formatLocationPathForDisplay(ideRootPath));
8✔
227
      }
228

229
    } else if (!isTest()) {
4!
230
      ideRootPath = getIdeRootPathFromEnv(true);
×
231
    }
232
    return ideRootPath;
2✔
233
  }
234

235
  /**
236
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
237
   */
238
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
239

240
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
241
    if (root != null) {
×
242
      Path rootPath = Path.of(root);
×
243
      if (Files.isDirectory(rootPath)) {
×
244
        Path absoluteRootPath = getFileAccess().toCanonicalPath(rootPath);
×
245
        if (withSanityCheck) {
×
246
          int nameCount = rootPath.getNameCount();
×
247
          int absoluteNameCount = absoluteRootPath.getNameCount();
×
248
          int delta = absoluteNameCount - nameCount;
×
249
          if (delta >= 0) {
×
250
            for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) {
×
251
              String rootName = rootPath.getName(nameIndex).toString();
×
252
              String absoluteRootName = absoluteRootPath.getName(nameIndex + delta).toString();
×
253
              if (!rootName.equals(absoluteRootName)) {
×
254
                warning("IDE_ROOT is set to {} but was expanded to absolute path {} and does not match for segment {} and {} - fix your IDEasy installation!",
×
255
                    formatLocationPathForDisplay(rootPath), formatLocationPathForDisplay(absoluteRootPath), rootName, absoluteRootName);
×
256
                break;
×
257
              }
258
            }
259
          } else {
260
            warning("IDE_ROOT is set to {} but was expanded to a shorter absolute path {}", formatLocationPathForDisplay(rootPath),
×
261
                formatLocationPathForDisplay(absoluteRootPath));
×
262
          }
263
        }
264
        return absoluteRootPath;
×
265
      } else if (withSanityCheck) {
×
266
        warning("IDE_ROOT is set to {} that is not an existing directory - fix your IDEasy installation!", formatLocationPathForDisplay(rootPath));
×
267
      }
268
    }
269
    return null;
×
270
  }
271

272
  @Override
273
  public void setCwd(Path userDir, String workspace, Path ideHome) {
274

275
    this.cwd = userDir;
3✔
276
    this.workspaceName = workspace;
3✔
277
    this.ideHome = ideHome;
3✔
278
    if (ideHome == null) {
2✔
279
      this.workspacePath = null;
3✔
280
      this.confPath = null;
3✔
281
      this.settingsPath = null;
3✔
282
      this.pluginsPath = null;
4✔
283
    } else {
284
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
285
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
286
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
287
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
288
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
289
    }
290
    if (isTest()) {
3!
291
      // only for testing...
292
      if (this.ideHome == null) {
3✔
293
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
294
      } else {
295
        this.userHome = this.ideHome.resolve("home");
7✔
296
      }
297
    } else {
298
      this.userHome = Path.of(getSystem().getProperty("user.home"));
×
299
    }
300
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
301
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
302

303
    this.path = computeSystemPath();
4✔
304
  }
1✔
305

306
  private String getMessageIdeHomeFound() {
307

308
    return "IDE environment variables have been set for " + this.ideHome + " in workspace " + this.workspaceName;
7✔
309
  }
310

311
  private String getMessageNotInsideIdeProject() {
312

313
    return "You are not inside an IDE project: " + formatLocationPathForDisplay(this.cwd);
6✔
314
  }
315

316
  private String getMessageIdeRootNotFound() {
317

318
    String root = getSystem().getEnv("IDE_ROOT");
5✔
319
    if (root == null) {
2!
320
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
321
    } else {
322
      return "The environment variable IDE_ROOT is pointing to an invalid path " + root + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
×
323
    }
324
  }
325

326
  /**
327
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
328
   */
329
  public boolean isTest() {
330

331
    return false;
×
332
  }
333

334
  protected SystemPath computeSystemPath() {
335

336
    return new SystemPath(this);
×
337
  }
338

339
  private boolean isIdeHome(Path dir) {
340

341
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
342
      return false;
2✔
343
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
344
      return false;
×
345
    }
346
    return true;
2✔
347
  }
348

349
  private EnvironmentVariables createVariables() {
350

351
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
352
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
353
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
354
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
355
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
356
    return conf.resolved();
3✔
357
  }
358

359
  protected AbstractEnvironmentVariables createSystemVariables() {
360

361
    return EnvironmentVariables.ofSystem(this);
3✔
362
  }
363

364
  @Override
365
  public SystemInfo getSystemInfo() {
366

367
    return this.systemInfo;
3✔
368
  }
369

370
  @Override
371
  public FileAccess getFileAccess() {
372

373
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
374
    configureNetworkProxy();
2✔
375
    return this.fileAccess;
3✔
376
  }
377

378
  @Override
379
  public CommandletManager getCommandletManager() {
380

381
    return this.commandletManager;
3✔
382
  }
383

384
  @Override
385
  public ToolRepository getDefaultToolRepository() {
386

387
    return this.defaultToolRepository;
3✔
388
  }
389

390
  @Override
391
  public MavenRepository getMavenToolRepository() {
392

393
    return this.mavenRepository;
3✔
394
  }
395

396
  @Override
397
  public CustomToolRepository getCustomToolRepository() {
398

399
    if (this.customToolRepository == null) {
3!
400
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
401
    }
402
    return this.customToolRepository;
3✔
403
  }
404

405
  @Override
406
  public Path getIdeHome() {
407

408
    return this.ideHome;
3✔
409
  }
410

411
  @Override
412
  public String getProjectName() {
413

414
    if (this.ideHome != null) {
3!
415
      return this.ideHome.getFileName().toString();
5✔
416
    }
417
    return "";
×
418
  }
419

420
  @Override
421
  public VersionIdentifier getProjectVersion() {
422

423
    if (this.ideHome != null) {
3!
424
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
425
      if (Files.exists(versionFile)) {
5✔
426
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
427
        return VersionIdentifier.of(version);
3✔
428
      }
429
    }
430
    return IdeMigrator.START_VERSION;
2✔
431
  }
432

433
  @Override
434
  public void setProjectVersion(VersionIdentifier version) {
435

436
    if (this.ideHome == null) {
3!
437
      throw new IllegalStateException("IDE_HOME not available!");
×
438
    }
439
    Objects.requireNonNull(version);
3✔
440
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
441
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
442
  }
1✔
443

444
  @Override
445
  public Path getIdeRoot() {
446

447
    return this.ideRoot;
3✔
448
  }
449

450
  @Override
451
  public Path getIdePath() {
452

453
    Path myIdeRoot = getIdeRoot();
3✔
454
    if (myIdeRoot == null) {
2!
455
      return null;
×
456
    }
457
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
458
  }
459

460
  @Override
461
  public Path getCwd() {
462

463
    return this.cwd;
3✔
464
  }
465

466
  @Override
467
  public Path getTempPath() {
468

469
    Path idePath = getIdePath();
3✔
470
    if (idePath == null) {
2!
471
      return null;
×
472
    }
473
    return idePath.resolve("tmp");
4✔
474
  }
475

476
  @Override
477
  public Path getTempDownloadPath() {
478

479
    Path tmp = getTempPath();
3✔
480
    if (tmp == null) {
2!
481
      return null;
×
482
    }
483
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
484
  }
485

486
  @Override
487
  public Path getUserHome() {
488

489
    return this.userHome;
3✔
490
  }
491

492
  @Override
493
  public Path getUserHomeIde() {
494

495
    return this.userHomeIde;
3✔
496
  }
497

498
  @Override
499
  public Path getSettingsPath() {
500

501
    return this.settingsPath;
3✔
502
  }
503

504
  @Override
505
  public Path getSettingsGitRepository() {
506

507
    Path settingsPath = getSettingsPath();
3✔
508
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
509
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
510
      error("Settings repository exists but is not a git repository.");
3✔
511
      return null;
2✔
512
    }
513
    return settingsPath;
2✔
514
  }
515

516
  @Override
517
  public boolean isSettingsRepositorySymlinkOrJunction() {
518

519
    Path settingsPath = getSettingsPath();
3✔
520
    if (settingsPath == null) {
2!
521
      return false;
×
522
    }
523
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
524
  }
525

526
  @Override
527
  public Path getSettingsCommitIdPath() {
528

529
    return this.settingsCommitIdPath;
3✔
530
  }
531

532
  @Override
533
  public Path getConfPath() {
534

535
    return this.confPath;
3✔
536
  }
537

538
  @Override
539
  public Path getSoftwarePath() {
540

541
    if (this.ideHome == null) {
3✔
542
      return null;
2✔
543
    }
544
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
545
  }
546

547
  @Override
548
  public Path getSoftwareExtraPath() {
549

550
    Path softwarePath = getSoftwarePath();
3✔
551
    if (softwarePath == null) {
2!
552
      return null;
×
553
    }
554
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
555
  }
556

557
  @Override
558
  public Path getSoftwareRepositoryPath() {
559

560
    Path idePath = getIdePath();
3✔
561
    if (idePath == null) {
2!
562
      return null;
×
563
    }
564
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
565
  }
566

567
  @Override
568
  public Path getPluginsPath() {
569

570
    return this.pluginsPath;
3✔
571
  }
572

573
  @Override
574
  public String getWorkspaceName() {
575

576
    return this.workspaceName;
3✔
577
  }
578

579
  @Override
580
  public Path getWorkspacePath() {
581

582
    return this.workspacePath;
3✔
583
  }
584

585
  @Override
586
  public Path getDownloadPath() {
587

588
    return this.downloadPath;
3✔
589
  }
590

591
  @Override
592
  public Path getUrlsPath() {
593

594
    Path idePath = getIdePath();
3✔
595
    if (idePath == null) {
2!
596
      return null;
×
597
    }
598
    return idePath.resolve(FOLDER_URLS);
4✔
599
  }
600

601
  @Override
602
  public Path getToolRepositoryPath() {
603

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

611
  @Override
612
  public SystemPath getPath() {
613

614
    return this.path;
3✔
615
  }
616

617
  @Override
618
  public EnvironmentVariables getVariables() {
619

620
    if (this.variables == null) {
3✔
621
      this.variables = createVariables();
4✔
622
    }
623
    return this.variables;
3✔
624
  }
625

626
  @Override
627
  public UrlMetadata getUrls() {
628

629
    if (this.urlMetadata == null) {
3✔
630
      if (!isTest()) {
3!
631
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
632
      }
633
      this.urlMetadata = new UrlMetadata(this);
6✔
634
    }
635
    return this.urlMetadata;
3✔
636
  }
637

638
  @Override
639
  public boolean isQuietMode() {
640

641
    return this.startContext.isQuietMode();
4✔
642
  }
643

644
  @Override
645
  public boolean isBatchMode() {
646

647
    return this.startContext.isBatchMode();
4✔
648
  }
649

650
  @Override
651
  public boolean isForceMode() {
652

653
    return this.startContext.isForceMode();
4✔
654
  }
655

656
  @Override
657
  public boolean isForcePull() {
658

659
    return this.startContext.isForcePull();
×
660
  }
661

662
  @Override
663
  public boolean isForcePlugins() {
664

665
    return this.startContext.isForcePlugins();
×
666
  }
667

668
  @Override
669
  public boolean isForceRepositories() {
670

671
    return this.startContext.isForceRepositories();
×
672
  }
673

674
  @Override
675
  public boolean isOfflineMode() {
676

677
    return this.startContext.isOfflineMode();
4✔
678
  }
679

680
  @Override
681
  public boolean isPrivacyMode() {
682
    return this.startContext.isPrivacyMode();
×
683
  }
684

685
  @Override
686
  public boolean isSkipUpdatesMode() {
687

688
    return this.startContext.isSkipUpdatesMode();
4✔
689
  }
690

691
  @Override
692
  public boolean isOnline() {
693

694
    if (this.online == null) {
3✔
695
      configureNetworkProxy();
2✔
696
      // we currently assume we have only a CLI process that runs shortly
697
      // therefore we run this check only once to save resources when this method is called many times
698
      try {
699
        int timeout = 1000;
2✔
700
        //open a connection to github.com and try to retrieve data
701
        //getContent fails if there is no connection
702
        URLConnection connection = new URL("https://www.github.com").openConnection();
6✔
703
        connection.setConnectTimeout(timeout);
3✔
704
        connection.getContent();
3✔
705
        this.online = Boolean.TRUE;
3✔
706
      } catch (Exception ignored) {
×
707
        this.online = Boolean.FALSE;
×
708
      }
1✔
709
    }
710
    return this.online.booleanValue();
4✔
711
  }
712

713
  private void configureNetworkProxy() {
714

715
    if (this.networkProxy == null) {
3✔
716
      this.networkProxy = new NetworkProxy(this);
6✔
717
      this.networkProxy.configure();
3✔
718
    }
719
  }
1✔
720

721
  @Override
722
  public Locale getLocale() {
723

724
    Locale locale = this.startContext.getLocale();
4✔
725
    if (locale == null) {
2✔
726
      locale = Locale.getDefault();
2✔
727
    }
728
    return locale;
2✔
729
  }
730

731
  @Override
732
  public DirectoryMerger getWorkspaceMerger() {
733

734
    if (this.workspaceMerger == null) {
3✔
735
      this.workspaceMerger = new DirectoryMerger(this);
6✔
736
    }
737
    return this.workspaceMerger;
3✔
738
  }
739

740
  /**
741
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
742
   */
743
  @Override
744
  public Path getDefaultExecutionDirectory() {
745

746
    return this.defaultExecutionDirectory;
×
747
  }
748

749
  /**
750
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
751
   */
752
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
753

754
    if (defaultExecutionDirectory != null) {
×
755
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
756
    }
757
  }
×
758

759
  @Override
760
  public GitContext getGitContext() {
761

762
    return new GitContextImpl(this);
×
763
  }
764

765
  @Override
766
  public ProcessContext newProcess() {
767

768
    ProcessContext processContext = createProcessContext();
3✔
769
    if (this.defaultExecutionDirectory != null) {
3!
770
      processContext.directory(this.defaultExecutionDirectory);
×
771
    }
772
    return processContext;
2✔
773
  }
774

775
  @Override
776
  public IdeSystem getSystem() {
777

778
    if (this.system == null) {
×
779
      this.system = new IdeSystemImpl(this);
×
780
    }
781
    return this.system;
×
782
  }
783

784
  /**
785
   * @return a new instance of {@link ProcessContext}.
786
   * @see #newProcess()
787
   */
788
  protected ProcessContext createProcessContext() {
789

790
    return new ProcessContextImpl(this);
5✔
791
  }
792

793
  @Override
794
  public IdeSubLogger level(IdeLogLevel level) {
795

796
    return this.startContext.level(level);
5✔
797
  }
798

799
  @Override
800
  public void logIdeHomeAndRootStatus() {
801
    if (this.ideRoot != null) {
3!
802
      success("IDE_ROOT is set to {}", formatLocationPathForDisplay(this.ideRoot));
×
803
    }
804
    if (this.ideHome == null) {
3✔
805
      warning(getMessageNotInsideIdeProject());
5✔
806
    } else {
807
      success("IDE_HOME is set to {}", formatLocationPathForDisplay(this.ideHome));
12✔
808
    }
809
  }
1✔
810

811
  /**
812
   * Formats a path for GDPR compliance based on the privacy mode.
813
   *
814
   * @param location Path to format.
815
   * @return the formatted path string.
816
   */
817
  public String formatLocationPathForDisplay(Path location) {
818
    String locationString = location.toString();
3✔
819
    if (this.startContext.isPrivacyMode()) {
4✔
820
      Path normalizedPath = location.normalize();
3✔
821

822
      if (this.ideHome != null && normalizedPath.startsWith(this.ideHome)) {
8!
823
        return Paths.get(IDE_HOME_PLACEHOLDER).resolve(this.ideHome.relativize(normalizedPath)).toString();
11✔
824
      }
825

826
      if (this.ideRoot != null && normalizedPath.startsWith(this.ideRoot)) {
×
827
        return Paths.get(IDE_ROOT_PLACEHOLDER).resolve(this.ideRoot.relativize(normalizedPath)).toString();
×
828
      }
829

830
      Path userHome = Path.of(getSystem().getProperty("user.home"));
×
831
      Path userHomePlaceholder = Paths.get(USER_HOME_PLACEHOLDER);
×
832
      if (this.userHome != null && normalizedPath.startsWith(this.userHome)) {
×
833
        return userHomePlaceholder.resolve(this.userHome.relativize(normalizedPath)).toString();
×
834
      } else if (normalizedPath.startsWith(userHome)) {
×
835
        return userHomePlaceholder.resolve(userHome.relativize(normalizedPath)).toString();
×
836
      }
837
      return location.toString();
×
838
    }
839
    return locationString;
2✔
840
  }
841

842
  @Override
843
  public String askForInput(String message, String defaultValue) {
844

845
    if (!message.isBlank()) {
×
846
      info(message);
×
847
    }
848
    if (isBatchMode()) {
×
849
      if (isForceMode() || isForcePull()) {
×
850
        return defaultValue;
×
851
      } else {
852
        throw new CliAbortException();
×
853
      }
854
    }
855
    String input = readLine().trim();
×
856
    return input.isEmpty() ? defaultValue : input;
×
857
  }
858

859
  @Override
860
  public String askForInput(String message) {
861

862
    String input;
863
    do {
864
      info(message);
3✔
865
      input = readLine().trim();
4✔
866
    } while (input.isEmpty());
3!
867

868
    return input;
2✔
869
  }
870

871
  @SuppressWarnings("unchecked")
872
  @Override
873
  public <O> O question(String question, O... options) {
874

875
    assert (options.length >= 2);
×
876
    interaction(question);
×
877
    Map<String, O> mapping = new HashMap<>(options.length);
×
878
    int i = 0;
×
879
    for (O option : options) {
×
880
      i++;
×
881
      String key = "" + option;
×
882
      addMapping(mapping, key, option);
×
883
      String numericKey = Integer.toString(i);
×
884
      if (numericKey.equals(key)) {
×
885
        trace("Options should not be numeric: " + key);
×
886
      } else {
887
        addMapping(mapping, numericKey, option);
×
888
      }
889
      interaction("Option " + numericKey + ": " + key);
×
890
    }
891
    O option = null;
×
892
    if (isBatchMode()) {
×
893
      if (isForceMode() || isForcePull()) {
×
894
        option = options[0];
×
895
        interaction("" + option);
×
896
      }
897
    } else {
898
      while (option == null) {
×
899
        String answer = readLine();
×
900
        option = mapping.get(answer);
×
901
        if (option == null) {
×
902
          warning("Invalid answer: '" + answer + "' - please try again.");
×
903
        }
904
      }
×
905
    }
906
    return option;
×
907
  }
908

909
  /**
910
   * @return the input from the end-user (e.g. read from the console).
911
   */
912
  protected abstract String readLine();
913

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

916
    O duplicate = mapping.put(key, option);
×
917
    if (duplicate != null) {
×
918
      throw new IllegalArgumentException("Duplicated option " + key);
×
919
    }
920
  }
×
921

922
  @Override
923
  public Step getCurrentStep() {
924

925
    return this.currentStep;
×
926
  }
927

928
  @Override
929
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
930

931
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
932
    return this.currentStep;
3✔
933
  }
934

935
  /**
936
   * Internal method to end the running {@link Step}.
937
   *
938
   * @param step the current {@link Step} to end.
939
   */
940
  public void endStep(StepImpl step) {
941

942
    if (step == this.currentStep) {
4!
943
      this.currentStep = this.currentStep.getParent();
6✔
944
    } else {
945
      String currentStepName = "null";
×
946
      if (this.currentStep != null) {
×
947
        currentStepName = this.currentStep.getName();
×
948
      }
949
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
950
    }
951
  }
1✔
952

953
  /**
954
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
955
   *
956
   * @param arguments the {@link CliArgument}.
957
   * @return the return code of the execution.
958
   */
959
  public int run(CliArguments arguments) {
960

961
    CliArgument current = arguments.current();
3✔
962
    assert (this.currentStep == null);
4!
963
    boolean supressStepSuccess = false;
2✔
964
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
965
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
966
    Commandlet cmd = null;
2✔
967
    ValidationResult result = null;
2✔
968
    try {
969
      while (commandletIterator.hasNext()) {
3✔
970
        cmd = commandletIterator.next();
4✔
971
        result = applyAndRun(arguments.copy(), cmd);
6✔
972
        if (result.isValid()) {
3!
973
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
974
          step.success();
2✔
975
          return ProcessResult.SUCCESS;
4✔
976
        }
977
      }
978
      this.startContext.activateLogging();
3✔
979
      verifyIdeMinVersion(false);
3✔
980
      if (result != null) {
2!
981
        error(result.getErrorMessage());
×
982
      }
983
      step.error("Invalid arguments: {}", current.getArgs());
10✔
984
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
985
      if (cmd != null) {
2!
986
        help.commandlet.setValue(cmd);
×
987
      }
988
      help.run();
2✔
989
      return 1;
4✔
990
    } catch (Throwable t) {
1✔
991
      this.startContext.activateLogging();
3✔
992
      step.error(t, true);
4✔
993
      throw t;
2✔
994
    } finally {
995
      step.close();
2✔
996
      assert (this.currentStep == null);
4!
997
      step.logSummary(supressStepSuccess);
3✔
998
    }
999
  }
1000

1001
  @Override
1002
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1003

1004
    this.startContext.deactivateLogging(threshold);
4✔
1005
    lambda.run();
2✔
1006
    this.startContext.activateLogging();
3✔
1007
  }
1✔
1008

1009
  /**
1010
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1011
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1012
   *     {@link Commandlet} did not match and we have to try a different candidate).
1013
   */
1014
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1015

1016
    IdeLogLevel previousLogLevel = null;
2✔
1017
    cmd.reset();
2✔
1018
    ValidationResult result = apply(arguments, cmd);
5✔
1019
    if (result.isValid()) {
3!
1020
      result = cmd.validate();
3✔
1021
    }
1022
    if (result.isValid()) {
3!
1023
      debug("Running commandlet {}", cmd);
9✔
1024
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1025
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1026
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1027
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1028
      }
1029
      try {
1030
        if (cmd.isProcessableOutput()) {
3!
1031
          if (!debug().isEnabled()) {
×
1032
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1033
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
1034
          }
1035
          this.startContext.activateLogging();
×
1036
        } else {
1037
          this.startContext.activateLogging();
3✔
1038
          if (cmd.isIdeHomeRequired()) {
3!
1039
            debug(getMessageIdeHomeFound());
4✔
1040
          }
1041
          Path settingsRepository = getSettingsGitRepository();
3✔
1042
          if (settingsRepository != null) {
2!
1043
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1044
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1045
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1046
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1047
                interaction(
×
1048
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
1049

1050
              } else {
1051
                interaction(
×
1052
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
1053
              }
1054
            }
1055
          }
1056
        }
1057
        boolean success = ensureLicenseAgreement(cmd);
4✔
1058
        if (!success) {
2!
1059
          return ValidationResultValid.get();
×
1060
        }
1061
        cmd.run();
2✔
1062
      } finally {
1063
        if (previousLogLevel != null) {
2!
1064
          this.startContext.setLogLevel(previousLogLevel);
×
1065
        }
1066
      }
1✔
1067
    } else {
1068
      trace("Commandlet did not match");
×
1069
    }
1070
    return result;
2✔
1071
  }
1072

1073
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1074

1075
    if (isTest()) {
3!
1076
      return true; // ignore for tests
2✔
1077
    }
1078
    getFileAccess().mkdirs(this.userHomeIde);
×
1079
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1080
    if (Files.isRegularFile(licenseAgreement)) {
×
1081
      return true; // success, license already accepted
×
1082
    }
1083
    if (cmd instanceof EnvironmentCommandlet) {
×
1084
      // 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
1085
      // 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
1086
      // printing anything anymore in such case.
1087
      return false;
×
1088
    }
1089
    boolean logLevelInfoDisabled = !this.startContext.info().isEnabled();
×
1090
    if (logLevelInfoDisabled) {
×
1091
      this.startContext.setLogLevel(IdeLogLevel.INFO, true);
×
1092
    }
1093
    boolean logLevelInteractionDisabled = !this.startContext.interaction().isEnabled();
×
1094
    if (logLevelInteractionDisabled) {
×
1095
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, true);
×
1096
    }
1097
    StringBuilder sb = new StringBuilder(1180);
×
1098
    sb.append(LOGO).append("""
×
1099
        Welcome to IDEasy!
1100
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1101
        It supports automatic download and installation of arbitrary 3rd party tools.
1102
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1103
        But if explicitly configured, also commercial software that requires an additional license may be used.
1104
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1105
        You are solely responsible for all risks implied by using this software.
1106
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1107
        You will be able to find it online under the following URL:
1108
        """).append(LICENSE_URL);
×
1109
    if (this.ideRoot != null) {
×
1110
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1111
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1112
    }
1113
    info(sb.toString());
×
1114
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1115

1116
    sb.setLength(0);
×
1117
    LocalDateTime now = LocalDateTime.now();
×
1118
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1119
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1120
    try {
1121
      Files.writeString(licenseAgreement, sb);
×
1122
    } catch (Exception e) {
×
1123
      throw new RuntimeException("Failed to save license agreement!", e);
×
1124
    }
×
1125
    if (logLevelInfoDisabled) {
×
1126
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
1127
    }
1128
    if (logLevelInteractionDisabled) {
×
1129
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
1130
    }
1131
    return true;
×
1132
  }
1133

1134
  @Override
1135
  public void verifyIdeMinVersion(boolean throwException) {
1136
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1137
    if (minVersion == null) {
2✔
1138
      return;
1✔
1139
    }
1140
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1141
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1142
          + "However, this is too old as your project requires at latest version %s\n"
1143
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1144
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1145
      if (throwException) {
2✔
1146
        throw new CliException(message);
5✔
1147
      } else {
1148
        warning(message);
3✔
1149
      }
1150
    }
1151
  }
1✔
1152

1153
  /**
1154
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1155
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1156
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1157
   */
1158
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1159

1160
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1161
    if (arguments.current().isStart()) {
4✔
1162
      arguments.next();
3✔
1163
    }
1164
    if (includeContextOptions) {
2✔
1165
      ContextCommandlet cc = new ContextCommandlet();
4✔
1166
      for (Property<?> property : cc.getProperties()) {
11✔
1167
        assert (property.isOption());
4!
1168
        property.apply(arguments, this, cc, collector);
7✔
1169
      }
1✔
1170
    }
1171
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1172
    CliArgument current = arguments.current();
3✔
1173
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1174
      collector.add(current.get(), null, null, null);
7✔
1175
    }
1176
    arguments.next();
3✔
1177
    while (commandletIterator.hasNext()) {
3✔
1178
      Commandlet cmd = commandletIterator.next();
4✔
1179
      if (!arguments.current().isEnd()) {
4✔
1180
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1181
      }
1182
    }
1✔
1183
    return collector.getSortedCandidates();
3✔
1184
  }
1185

1186
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1187

1188
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
1189
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1190
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1191
    List<Property<?>> properties = cmd.getProperties();
3✔
1192
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1193
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1194
    for (Property<?> property : properties) {
10✔
1195
      if (property.isOption()) {
3✔
1196
        optionProperties.add(property);
4✔
1197
      }
1198
    }
1✔
1199
    CliArgument currentArgument = arguments.current();
3✔
1200
    while (!currentArgument.isEnd()) {
3✔
1201
      trace("Trying to match argument '{}'", currentArgument);
9✔
1202
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1203
        if (currentArgument.isCompletion()) {
3✔
1204
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1205
          while (optionIterator.hasNext()) {
3✔
1206
            Property<?> option = optionIterator.next();
4✔
1207
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1208
            if (success) {
2✔
1209
              optionIterator.remove();
2✔
1210
              arguments.next();
3✔
1211
            }
1212
          }
1✔
1213
        } else {
1✔
1214
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1215
          if (option != null) {
2✔
1216
            arguments.next();
3✔
1217
            boolean removed = optionProperties.remove(option);
4✔
1218
            if (!removed) {
2!
1219
              option = null;
×
1220
            }
1221
          }
1222
          if (option == null) {
2✔
1223
            trace("No such option was found.");
3✔
1224
            return;
1✔
1225
          }
1226
        }
1✔
1227
      } else {
1228
        if (valueIterator.hasNext()) {
3✔
1229
          Property<?> valueProperty = valueIterator.next();
4✔
1230
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1231
          if (!success) {
2✔
1232
            trace("Completion cannot match any further.");
3✔
1233
            return;
1✔
1234
          }
1235
        } else {
1✔
1236
          trace("No value left for completion.");
3✔
1237
          return;
1✔
1238
        }
1239
      }
1240
      currentArgument = arguments.current();
4✔
1241
    }
1242
  }
1✔
1243

1244
  /**
1245
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1246
   *     {@link CliArguments#copy() copy} as needed.
1247
   * @param cmd the potential {@link Commandlet} to match.
1248
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1249
   */
1250
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1251

1252
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1253
    CliArgument currentArgument = arguments.current();
3✔
1254
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1255
    Property<?> property = null;
2✔
1256
    if (propertyIterator.hasNext()) {
3!
1257
      property = propertyIterator.next();
4✔
1258
    }
1259
    while (!currentArgument.isEnd()) {
3✔
1260
      trace("Trying to match argument '{}'", currentArgument);
9✔
1261
      Property<?> currentProperty = property;
2✔
1262
      if (!arguments.isEndOptions()) {
3!
1263
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1264
        if (option != null) {
2!
1265
          currentProperty = option;
×
1266
        }
1267
      }
1268
      if (currentProperty == null) {
2!
1269
        trace("No option or next value found");
×
1270
        ValidationState state = new ValidationState(null);
×
1271
        state.addErrorMessage("No matching property found");
×
1272
        return state;
×
1273
      }
1274
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1275
      if (currentProperty == property) {
3!
1276
        if (!property.isMultiValued()) {
3✔
1277
          if (propertyIterator.hasNext()) {
3✔
1278
            property = propertyIterator.next();
5✔
1279
          } else {
1280
            property = null;
2✔
1281
          }
1282
        }
1283
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1284
          arguments.stopSplitShortOptions();
2✔
1285
        }
1286
      }
1287
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1288
      if (!matches) {
2!
1289
        ValidationState state = new ValidationState(null);
×
1290
        state.addErrorMessage("No matching property found");
×
1291
        return state;
×
1292
      }
1293
      currentArgument = arguments.current();
3✔
1294
    }
1✔
1295
    return ValidationResultValid.get();
2✔
1296
  }
1297

1298
  @Override
1299
  public String findBash() {
1300

1301
    String bash = "bash";
2✔
1302
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1303
      bash = findBashOnWindows();
×
1304
    }
1305

1306
    return bash;
2✔
1307
  }
1308

1309
  private String findBashOnWindows() {
1310

1311
    // Check if Git Bash exists in the default location
1312
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1313
    if (Files.exists(defaultPath)) {
×
1314
      return defaultPath.toString();
×
1315
    }
1316

1317
    // If not found in the default location, try the registry query
1318
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1319
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1320
    String regQueryResult;
1321
    for (String bashVariant : bashVariants) {
×
1322
      for (String registryKey : registryKeys) {
×
1323
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1324
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1325

1326
        try {
1327
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1328
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1329
            StringBuilder output = new StringBuilder();
×
1330
            String line;
1331

1332
            while ((line = reader.readLine()) != null) {
×
1333
              output.append(line);
×
1334
            }
1335

1336
            int exitCode = process.waitFor();
×
1337
            if (exitCode != 0) {
×
1338
              return null;
×
1339
            }
1340

1341
            regQueryResult = output.toString();
×
1342
            if (regQueryResult != null) {
×
1343
              int index = regQueryResult.indexOf("REG_SZ");
×
1344
              if (index != -1) {
×
1345
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1346
                return path + "\\bin\\bash.exe";
×
1347
              }
1348
            }
1349

1350
          }
×
1351
        } catch (Exception e) {
×
1352
          return null;
×
1353
        }
×
1354
      }
1355
    }
1356
    // no bash found
1357
    return null;
×
1358
  }
1359

1360
  @Override
1361
  public WindowsPathSyntax getPathSyntax() {
1362

1363
    return this.pathSyntax;
3✔
1364
  }
1365

1366
  /**
1367
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1368
   */
1369
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1370

1371
    this.pathSyntax = pathSyntax;
3✔
1372
  }
1✔
1373

1374
  /**
1375
   * @return the {@link IdeStartContextImpl}.
1376
   */
1377
  public IdeStartContextImpl getStartContext() {
1378

1379
    return startContext;
3✔
1380
  }
1381

1382
  /**
1383
   * @return the {@link WindowsHelper}.
1384
   */
1385
  public final WindowsHelper getWindowsHelper() {
1386

1387
    if (this.windowsHelper == null) {
3✔
1388
      this.windowsHelper = createWindowsHelper();
4✔
1389
    }
1390
    return this.windowsHelper;
3✔
1391
  }
1392

1393
  /**
1394
   * @return the new {@link WindowsHelper} instance.
1395
   */
1396
  protected WindowsHelper createWindowsHelper() {
1397

1398
    return new WindowsHelperImpl(this);
×
1399
  }
1400

1401
  /**
1402
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1403
   */
1404
  public void reload() {
1405

1406
    this.variables = null;
3✔
1407
    this.customToolRepository = null;
3✔
1408
  }
1✔
1409

1410
  @Override
1411
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1412

1413
    assert (Files.isDirectory(installationPath));
6!
1414
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1415
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1416
  }
1✔
1417

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