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

devonfw / IDEasy / 16754549568

05 Aug 2025 03:29PM UTC coverage: 69.457% (+1.0%) from 68.446%
16754549568

Pull #1418

github

web-flow
Merge a1115a3da into d13807b57
Pull Request #1418: Use WindowsHelper to find bash instead of direct ProcessBuilder

3338 of 5242 branches covered (63.68%)

Branch coverage included in aggregate %.

8617 of 11970 relevant lines covered (71.99%)

3.17 hits per line

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

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

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

5

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

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

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

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

84
  private static final String LICENSE_URL = "https://github.com/devonfw/IDEasy/blob/main/documentation/LICENSE.adoc";
85

86
  private final IdeStartContextImpl startContext;
87

88
  private Path ideHome;
89

90
  private final Path ideRoot;
91

92
  private Path confPath;
93

94
  protected Path settingsPath;
95

96
  private Path settingsCommitIdPath;
97

98
  protected Path pluginsPath;
99

100
  private Path workspacePath;
101

102
  private String workspaceName;
103

104
  private Path cwd;
105

106
  private Path downloadPath;
107

108
  private Path userHome;
109

110
  private Path userHomeIde;
111

112
  private SystemPath path;
113

114
  private WindowsPathSyntax pathSyntax;
115

116
  private final SystemInfo systemInfo;
117

118
  private EnvironmentVariables variables;
119

120
  private final FileAccess fileAccess;
121

122
  protected CommandletManager commandletManager;
123

124
  protected ToolRepository defaultToolRepository;
125

126
  private CustomToolRepository customToolRepository;
127

128
  private final MavenRepository mavenRepository;
129

130
  private DirectoryMerger workspaceMerger;
131

132
  protected UrlMetadata urlMetadata;
133

134
  protected Path defaultExecutionDirectory;
135

136
  private StepImpl currentStep;
137

138
  protected Boolean online;
139

140
  protected IdeSystem system;
141

142
  private NetworkProxy networkProxy;
143

144
  private WindowsHelper windowsHelper;
145

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

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

206
    setCwd(workingDirectory, workspace, currentDir);
5✔
207

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

217
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
218
    this.mavenRepository = new MavenRepository(this);
6✔
219
  }
1✔
220

221
  private Path findIdeRoot(Path ideHomePath) {
222

223
    Path ideRootPath = null;
2✔
224
    if (ideHomePath != null) {
2✔
225
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
226
      ideRootPath = ideHomePath.getParent();
3✔
227
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
8!
228
        warning(
12✔
229
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
230
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
231
            ideRootPathFromEnv,
232
            ideHomePath.getFileName(), ideRootPath);
6✔
233
      }
234
    } else if (!isTest()) {
4!
235
      ideRootPath = getIdeRootPathFromEnv(true);
×
236
    }
237
    return ideRootPath;
2✔
238
  }
239

240
  /**
241
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
242
   */
243
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
244

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

277
  @Override
278
  public void setCwd(Path userDir, String workspace, Path ideHome) {
279

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

309
  private String getMessageIdeHomeFound() {
310

311
    String wks = this.workspaceName;
3✔
312
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
313
      wks = "*".repeat(wks.length());
×
314
    }
315
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
316
  }
317

318
  private String getMessageNotInsideIdeProject() {
319

320
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
321
  }
322

323
  private String getMessageIdeRootNotFound() {
324

325
    String root = getSystem().getEnv("IDE_ROOT");
5✔
326
    if (root == null) {
2!
327
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
328
    } else {
329
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
330
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
331
    }
332
  }
333

334
  /**
335
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
336
   */
337
  public boolean isTest() {
338

339
    return false;
×
340
  }
341

342
  protected SystemPath computeSystemPath() {
343

344
    return new SystemPath(this);
×
345
  }
346

347
  private boolean isIdeHome(Path dir) {
348

349
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
350
      return false;
2✔
351
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
352
      return false;
×
353
    }
354
    return true;
2✔
355
  }
356

357
  private EnvironmentVariables createVariables() {
358

359
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
360
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
361
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
362
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
363
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
364
    return conf.resolved();
3✔
365
  }
366

367
  protected AbstractEnvironmentVariables createSystemVariables() {
368

369
    return EnvironmentVariables.ofSystem(this);
3✔
370
  }
371

372
  @Override
373
  public SystemInfo getSystemInfo() {
374

375
    return this.systemInfo;
3✔
376
  }
377

378
  @Override
379
  public FileAccess getFileAccess() {
380

381
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
382
    configureNetworkProxy();
2✔
383
    return this.fileAccess;
3✔
384
  }
385

386
  @Override
387
  public CommandletManager getCommandletManager() {
388

389
    return this.commandletManager;
3✔
390
  }
391

392
  @Override
393
  public ToolRepository getDefaultToolRepository() {
394

395
    return this.defaultToolRepository;
3✔
396
  }
397

398
  @Override
399
  public MavenRepository getMavenToolRepository() {
400

401
    return this.mavenRepository;
3✔
402
  }
403

404
  @Override
405
  public CustomToolRepository getCustomToolRepository() {
406

407
    if (this.customToolRepository == null) {
3!
408
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
409
    }
410
    return this.customToolRepository;
3✔
411
  }
412

413
  @Override
414
  public Path getIdeHome() {
415

416
    return this.ideHome;
3✔
417
  }
418

419
  @Override
420
  public String getProjectName() {
421

422
    if (this.ideHome != null) {
3!
423
      return this.ideHome.getFileName().toString();
5✔
424
    }
425
    return "";
×
426
  }
427

428
  @Override
429
  public VersionIdentifier getProjectVersion() {
430

431
    if (this.ideHome != null) {
3!
432
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
433
      if (Files.exists(versionFile)) {
5✔
434
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
435
        return VersionIdentifier.of(version);
3✔
436
      }
437
    }
438
    return IdeMigrator.START_VERSION;
2✔
439
  }
440

441
  @Override
442
  public void setProjectVersion(VersionIdentifier version) {
443

444
    if (this.ideHome == null) {
3!
445
      throw new IllegalStateException("IDE_HOME not available!");
×
446
    }
447
    Objects.requireNonNull(version);
3✔
448
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
449
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
450
  }
1✔
451

452
  @Override
453
  public Path getIdeRoot() {
454

455
    return this.ideRoot;
3✔
456
  }
457

458
  @Override
459
  public Path getIdePath() {
460

461
    Path myIdeRoot = getIdeRoot();
3✔
462
    if (myIdeRoot == null) {
2!
463
      return null;
×
464
    }
465
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
466
  }
467

468
  @Override
469
  public Path getCwd() {
470

471
    return this.cwd;
3✔
472
  }
473

474
  @Override
475
  public Path getTempPath() {
476

477
    Path idePath = getIdePath();
3✔
478
    if (idePath == null) {
2!
479
      return null;
×
480
    }
481
    return idePath.resolve("tmp");
4✔
482
  }
483

484
  @Override
485
  public Path getTempDownloadPath() {
486

487
    Path tmp = getTempPath();
3✔
488
    if (tmp == null) {
2!
489
      return null;
×
490
    }
491
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
492
  }
493

494
  @Override
495
  public Path getUserHome() {
496

497
    return this.userHome;
3✔
498
  }
499

500
  /**
501
   * This method should only be used for tests to mock user home.
502
   *
503
   * @param userHome the new value of {@link #getUserHome()}.
504
   */
505
  protected void setUserHome(Path userHome) {
506

507
    this.userHome = userHome;
3✔
508
    resetPrivacyMap();
2✔
509
  }
1✔
510

511
  @Override
512
  public Path getUserHomeIde() {
513

514
    return this.userHomeIde;
3✔
515
  }
516

517
  @Override
518
  public Path getSettingsPath() {
519

520
    return this.settingsPath;
3✔
521
  }
522

523
  @Override
524
  public Path getSettingsGitRepository() {
525

526
    Path settingsPath = getSettingsPath();
3✔
527
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
528
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
529
      error("Settings repository exists but is not a git repository.");
3✔
530
      return null;
2✔
531
    }
532
    return settingsPath;
2✔
533
  }
534

535
  @Override
536
  public boolean isSettingsRepositorySymlinkOrJunction() {
537

538
    Path settingsPath = getSettingsPath();
3✔
539
    if (settingsPath == null) {
2!
540
      return false;
×
541
    }
542
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
543
  }
544

545
  @Override
546
  public Path getSettingsCommitIdPath() {
547

548
    return this.settingsCommitIdPath;
3✔
549
  }
550

551
  @Override
552
  public Path getConfPath() {
553

554
    return this.confPath;
3✔
555
  }
556

557
  @Override
558
  public Path getSoftwarePath() {
559

560
    if (this.ideHome == null) {
3✔
561
      return null;
2✔
562
    }
563
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
564
  }
565

566
  @Override
567
  public Path getSoftwareExtraPath() {
568

569
    Path softwarePath = getSoftwarePath();
3✔
570
    if (softwarePath == null) {
2!
571
      return null;
×
572
    }
573
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
574
  }
575

576
  @Override
577
  public Path getSoftwareRepositoryPath() {
578

579
    Path idePath = getIdePath();
3✔
580
    if (idePath == null) {
2!
581
      return null;
×
582
    }
583
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
584
  }
585

586
  @Override
587
  public Path getPluginsPath() {
588

589
    return this.pluginsPath;
3✔
590
  }
591

592
  @Override
593
  public String getWorkspaceName() {
594

595
    return this.workspaceName;
3✔
596
  }
597

598
  @Override
599
  public Path getWorkspacePath() {
600

601
    return this.workspacePath;
3✔
602
  }
603

604
  @Override
605
  public Path getDownloadPath() {
606

607
    return this.downloadPath;
3✔
608
  }
609

610
  @Override
611
  public Path getUrlsPath() {
612

613
    Path idePath = getIdePath();
3✔
614
    if (idePath == null) {
2!
615
      return null;
×
616
    }
617
    return idePath.resolve(FOLDER_URLS);
4✔
618
  }
619

620
  @Override
621
  public Path getToolRepositoryPath() {
622

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

630
  @Override
631
  public SystemPath getPath() {
632

633
    return this.path;
3✔
634
  }
635

636
  @Override
637
  public EnvironmentVariables getVariables() {
638

639
    if (this.variables == null) {
3✔
640
      this.variables = createVariables();
4✔
641
    }
642
    return this.variables;
3✔
643
  }
644

645
  @Override
646
  public UrlMetadata getUrls() {
647

648
    if (this.urlMetadata == null) {
3✔
649
      if (!isTest()) {
3!
650
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
651
      }
652
      this.urlMetadata = new UrlMetadata(this);
6✔
653
    }
654
    return this.urlMetadata;
3✔
655
  }
656

657
  @Override
658
  public boolean isQuietMode() {
659

660
    return this.startContext.isQuietMode();
4✔
661
  }
662

663
  @Override
664
  public boolean isBatchMode() {
665

666
    return this.startContext.isBatchMode();
4✔
667
  }
668

669
  @Override
670
  public boolean isForceMode() {
671

672
    return this.startContext.isForceMode();
4✔
673
  }
674

675
  @Override
676
  public boolean isForcePull() {
677

678
    return this.startContext.isForcePull();
4✔
679
  }
680

681
  @Override
682
  public boolean isForcePlugins() {
683

684
    return this.startContext.isForcePlugins();
4✔
685
  }
686

687
  @Override
688
  public boolean isForceRepositories() {
689

690
    return this.startContext.isForceRepositories();
4✔
691
  }
692

693
  @Override
694
  public boolean isOfflineMode() {
695

696
    return this.startContext.isOfflineMode();
4✔
697
  }
698

699
  @Override
700
  public boolean isPrivacyMode() {
701
    return this.startContext.isPrivacyMode();
4✔
702
  }
703

704
  @Override
705
  public boolean isSkipUpdatesMode() {
706

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

710
  @Override
711
  public boolean isOnline() {
712

713
    if (this.online == null) {
3✔
714
      configureNetworkProxy();
2✔
715
      // we currently assume we have only a CLI process that runs shortly
716
      // therefore we run this check only once to save resources when this method is called many times
717
      String url = "https://www.github.com";
2✔
718
      try {
719
        int timeout = 1000;
2✔
720
        //open a connection to github.com and try to retrieve data
721
        //getContent fails if there is no connection
722
        URLConnection connection = new URL(url).openConnection();
6✔
723
        connection.setConnectTimeout(timeout);
3✔
724
        connection.getContent();
3✔
725
        this.online = Boolean.TRUE;
3✔
726
      } catch (Exception e) {
×
727
        if (debug().isEnabled()) {
×
728
          debug().log(e, "Error when trying to connect to {}", url);
×
729
        }
730
        this.online = Boolean.FALSE;
×
731
      }
1✔
732
    }
733
    return this.online.booleanValue();
4✔
734
  }
735

736
  private void configureNetworkProxy() {
737

738
    if (this.networkProxy == null) {
3✔
739
      this.networkProxy = new NetworkProxy(this);
6✔
740
      this.networkProxy.configure();
3✔
741
    }
742
  }
1✔
743

744
  @Override
745
  public Locale getLocale() {
746

747
    Locale locale = this.startContext.getLocale();
4✔
748
    if (locale == null) {
2✔
749
      locale = Locale.getDefault();
2✔
750
    }
751
    return locale;
2✔
752
  }
753

754
  @Override
755
  public DirectoryMerger getWorkspaceMerger() {
756

757
    if (this.workspaceMerger == null) {
3✔
758
      this.workspaceMerger = new DirectoryMerger(this);
6✔
759
    }
760
    return this.workspaceMerger;
3✔
761
  }
762

763
  /**
764
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
765
   */
766
  @Override
767
  public Path getDefaultExecutionDirectory() {
768

769
    return this.defaultExecutionDirectory;
×
770
  }
771

772
  /**
773
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
774
   */
775
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
776

777
    if (defaultExecutionDirectory != null) {
×
778
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
779
    }
780
  }
×
781

782
  @Override
783
  public GitContext getGitContext() {
784

785
    return new GitContextImpl(this);
×
786
  }
787

788
  @Override
789
  public ProcessContext newProcess() {
790

791
    ProcessContext processContext = createProcessContext();
3✔
792
    if (this.defaultExecutionDirectory != null) {
3!
793
      processContext.directory(this.defaultExecutionDirectory);
×
794
    }
795
    return processContext;
2✔
796
  }
797

798
  @Override
799
  public IdeSystem getSystem() {
800

801
    if (this.system == null) {
×
802
      this.system = new IdeSystemImpl(this);
×
803
    }
804
    return this.system;
×
805
  }
806

807
  /**
808
   * @return a new instance of {@link ProcessContext}.
809
   * @see #newProcess()
810
   */
811
  protected ProcessContext createProcessContext() {
812

813
    return new ProcessContextImpl(this);
5✔
814
  }
815

816
  @Override
817
  public IdeSubLogger level(IdeLogLevel level) {
818

819
    return this.startContext.level(level);
5✔
820
  }
821

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

834
  @Override
835
  public String formatArgument(Object argument) {
836

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

857
  /**
858
   * @param path the sensitive {@link Path} to
859
   * @param replacement the replacement to mask the {@link Path} in log output.
860
   */
861
  protected void initializePrivacyMap(Path path, String replacement) {
862

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

874
  /**
875
   * Resets the privacy map in case fundamental values have changed.
876
   */
877
  private void resetPrivacyMap() {
878

879
    this.privacyMap.clear();
3✔
880
  }
1✔
881

882

883
  @Override
884
  public String askForInput(String message, String defaultValue) {
885

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

908
  @SuppressWarnings("unchecked")
909
  @Override
910
  public <O> O question(O[] options, String question, Object... args) {
911

912
    assert (options.length >= 2);
5!
913
    interaction(question, args);
4✔
914
    return displayOptionsAndGetAnswer(options);
4✔
915
  }
916

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

950
  /**
951
   * @return the input from the end-user (e.g. read from the console).
952
   */
953
  protected abstract String readLine();
954

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

957
    O duplicate = mapping.put(key, option);
5✔
958
    if (duplicate != null) {
2!
959
      throw new IllegalArgumentException("Duplicated option " + key);
×
960
    }
961
  }
1✔
962

963
  @Override
964
  public Step getCurrentStep() {
965

966
    return this.currentStep;
×
967
  }
968

969
  @Override
970
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
971

972
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
973
    return this.currentStep;
3✔
974
  }
975

976
  /**
977
   * Internal method to end the running {@link Step}.
978
   *
979
   * @param step the current {@link Step} to end.
980
   */
981
  public void endStep(StepImpl step) {
982

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

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

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

1042
  @Override
1043
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1044

1045
    this.startContext.deactivateLogging(threshold);
4✔
1046
    lambda.run();
2✔
1047
    this.startContext.activateLogging();
3✔
1048
  }
1✔
1049

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

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

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

1114
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1115

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

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

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

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

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

1227
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1228

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

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

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

1339
  @Override
1340
  public String findBash() {
1341

1342
    String bash = "bash";
2✔
1343
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1344
      bash = findBashOnWindows();
×
1345
    }
1346

1347
    return bash;
2✔
1348
  }
1349

1350
  private String findBashOnWindows() {
1351

1352
    // Check if Git Bash exists in the default location
1353
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1354
    if (Files.exists(defaultPath)) {
×
1355
      return defaultPath.toString();
×
1356
    }
1357

1358
    // If not found in the default location, try the registry query
1359
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1360
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1361
    for (String bashVariant : bashVariants) {
×
1362
      for (String registryKey : registryKeys) {
×
1363
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1364
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1365
        
1366
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1367
        if (path != null) {
×
1368
          return path + "\\bin\\bash.exe";
×
1369
        }
1370
      }
1371
    }
1372
    // no bash found
1373
    return null;
×
1374
  }
1375

1376
  @Override
1377
  public WindowsPathSyntax getPathSyntax() {
1378

1379
    return this.pathSyntax;
3✔
1380
  }
1381

1382
  /**
1383
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1384
   */
1385
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1386

1387
    this.pathSyntax = pathSyntax;
3✔
1388
  }
1✔
1389

1390
  /**
1391
   * @return the {@link IdeStartContextImpl}.
1392
   */
1393
  public IdeStartContextImpl getStartContext() {
1394

1395
    return startContext;
3✔
1396
  }
1397

1398
  /**
1399
   * @return the {@link WindowsHelper}.
1400
   */
1401
  public final WindowsHelper getWindowsHelper() {
1402

1403
    if (this.windowsHelper == null) {
3✔
1404
      this.windowsHelper = createWindowsHelper();
4✔
1405
    }
1406
    return this.windowsHelper;
3✔
1407
  }
1408

1409
  /**
1410
   * @return the new {@link WindowsHelper} instance.
1411
   */
1412
  protected WindowsHelper createWindowsHelper() {
1413

1414
    return new WindowsHelperImpl(this);
×
1415
  }
1416

1417
  /**
1418
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1419
   */
1420
  public void reload() {
1421

1422
    this.variables = null;
3✔
1423
    this.customToolRepository = null;
3✔
1424
  }
1✔
1425

1426
  @Override
1427
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1428

1429
    assert (Files.isDirectory(installationPath));
6!
1430
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1431
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1432
  }
1✔
1433

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