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

devonfw / IDEasy / 16329054956

16 Jul 2025 07:57PM UTC coverage: 68.56% (+0.1%) from 68.446%
16329054956

Pull #1418

github

web-flow
Merge 239fa437f into ab7eb024e
Pull Request #1418: Use WindowsHelper to find bash instead of direct ProcessBuilder

3287 of 5196 branches covered (63.26%)

Branch coverage included in aggregate %.

8410 of 11865 relevant lines covered (70.88%)

3.13 hits per line

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

66.63
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();
×
679
  }
680

681
  @Override
682
  public boolean isForcePlugins() {
683

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

687
  @Override
688
  public boolean isForceRepositories() {
689

690
    return this.startContext.isForceRepositories();
×
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
    if (!message.isBlank()) {
3!
887
      info(message);
3✔
888
    }
889
    if (isBatchMode()) {
3!
890
      if (isForceMode() || isForcePull()) {
×
891
        return defaultValue;
×
892
      } else {
893
        throw new CliAbortException();
×
894
      }
895
    }
896
    String input = readLine().trim();
4✔
897
    return input.isEmpty() ? defaultValue : input;
5!
898
  }
899

900
  @Override
901
  public String askForInput(String message) {
902

903
    String input;
904
    do {
905
      info(message);
3✔
906
      input = readLine().trim();
4✔
907
    } while (input.isEmpty());
3!
908

909
    return input;
2✔
910
  }
911

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

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

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

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

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

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

967
  @Override
968
  public Step getCurrentStep() {
969

970
    return this.currentStep;
×
971
  }
972

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

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

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

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

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

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

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

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

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

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

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

1118
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1119

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

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

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

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

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

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

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

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

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

1343
  @Override
1344
  public String findBash() {
1345

1346
    String bash = "bash";
2✔
1347
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1348
      bash = findBashOnWindows();
×
1349
    }
1350

1351
    return bash;
2✔
1352
  }
1353

1354
  private String findBashOnWindows() {
1355

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

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

1380
  @Override
1381
  public WindowsPathSyntax getPathSyntax() {
1382

1383
    return this.pathSyntax;
3✔
1384
  }
1385

1386
  /**
1387
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1388
   */
1389
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1390

1391
    this.pathSyntax = pathSyntax;
3✔
1392
  }
1✔
1393

1394
  /**
1395
   * @return the {@link IdeStartContextImpl}.
1396
   */
1397
  public IdeStartContextImpl getStartContext() {
1398

1399
    return startContext;
3✔
1400
  }
1401

1402
  /**
1403
   * @return the {@link WindowsHelper}.
1404
   */
1405
  public final WindowsHelper getWindowsHelper() {
1406

1407
    if (this.windowsHelper == null) {
3✔
1408
      this.windowsHelper = createWindowsHelper();
4✔
1409
    }
1410
    return this.windowsHelper;
3✔
1411
  }
1412

1413
  /**
1414
   * @return the new {@link WindowsHelper} instance.
1415
   */
1416
  protected WindowsHelper createWindowsHelper() {
1417

1418
    return new WindowsHelperImpl(this);
×
1419
  }
1420

1421
  /**
1422
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1423
   */
1424
  public void reload() {
1425

1426
    this.variables = null;
3✔
1427
    this.customToolRepository = null;
3✔
1428
  }
1✔
1429

1430
  @Override
1431
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1432

1433
    assert (Files.isDirectory(installationPath));
6!
1434
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1435
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1436
  }
1✔
1437

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