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

devonfw / IDEasy / 16118323023

07 Jul 2025 01:28PM UTC coverage: 68.154% (-0.3%) from 68.446%
16118323023

Pull #1017

github

web-flow
Merge 540717d63 into ab7eb024e
Pull Request #1017: #404: enhance logging with custom slf4j bridge

3307 of 5246 branches covered (63.04%)

Branch coverage included in aggregate %.

8470 of 12034 relevant lines covered (70.38%)

3.1 hits per line

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

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

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

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

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

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

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

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

87
  private final IdeStartContextImpl startContext;
88

89
  private Path ideHome;
90

91
  private final Path ideRoot;
92

93
  private Path confPath;
94

95
  protected Path settingsPath;
96

97
  private Path settingsCommitIdPath;
98

99
  protected Path pluginsPath;
100

101
  private Path workspacePath;
102

103
  private String workspaceName;
104

105
  private Path cwd;
106

107
  private Path downloadPath;
108

109
  private Path userHome;
110

111
  private Path userHomeIde;
112

113
  private SystemPath path;
114

115
  private WindowsPathSyntax pathSyntax;
116

117
  private final SystemInfo systemInfo;
118

119
  private EnvironmentVariables variables;
120

121
  private final FileAccess fileAccess;
122

123
  protected CommandletManager commandletManager;
124

125
  protected ToolRepository defaultToolRepository;
126

127
  private CustomToolRepository customToolRepository;
128

129
  private final MavenRepository mavenRepository;
130

131
  private DirectoryMerger workspaceMerger;
132

133
  protected UrlMetadata urlMetadata;
134

135
  protected Path defaultExecutionDirectory;
136

137
  private StepImpl currentStep;
138

139
  protected Boolean online;
140

141
  protected IdeSystem system;
142

143
  private NetworkProxy networkProxy;
144

145
  private WindowsHelper windowsHelper;
146

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

149
  /** Context used for logging */
150
  private static IdeContext loggingContext;
151

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

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

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

210
    setCwd(workingDirectory, workspace, currentDir);
5✔
211

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

221
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
222
    loggingContext = this;
2✔
223
    this.mavenRepository = new MavenRepository(this);
6✔
224
  }
1✔
225

226
  private Path findIdeRoot(Path ideHomePath) {
227

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

245
  /**
246
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
247
   */
248
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
249

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

282
  @Override
283
  public void setCwd(Path userDir, String workspace, Path ideHome) {
284

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

314
  private String getMessageIdeHomeFound() {
315

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

323
  private String getMessageNotInsideIdeProject() {
324

325
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
326
  }
327

328
  private String getMessageIdeRootNotFound() {
329

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

339
  /**
340
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
341
   */
342
  public boolean isTest() {
343

344
    return false;
×
345
  }
346

347
  protected SystemPath computeSystemPath() {
348

349
    return new SystemPath(this);
×
350
  }
351

352
  private boolean isIdeHome(Path dir) {
353

354
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
355
      return false;
2✔
356
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
357
      return false;
×
358
    }
359
    return true;
2✔
360
  }
361

362
  private EnvironmentVariables createVariables() {
363

364
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
365
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
366
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
367
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
368
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
369
    return conf.resolved();
3✔
370
  }
371

372
  protected AbstractEnvironmentVariables createSystemVariables() {
373

374
    return EnvironmentVariables.ofSystem(this);
3✔
375
  }
376

377
  @Override
378
  public SystemInfo getSystemInfo() {
379

380
    return this.systemInfo;
3✔
381
  }
382

383
  @Override
384
  public FileAccess getFileAccess() {
385

386
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
387
    configureNetworkProxy();
2✔
388
    return this.fileAccess;
3✔
389
  }
390

391
  @Override
392
  public CommandletManager getCommandletManager() {
393

394
    return this.commandletManager;
3✔
395
  }
396

397
  @Override
398
  public ToolRepository getDefaultToolRepository() {
399

400
    return this.defaultToolRepository;
3✔
401
  }
402

403
  @Override
404
  public MavenRepository getMavenToolRepository() {
405

406
    return this.mavenRepository;
3✔
407
  }
408

409
  @Override
410
  public CustomToolRepository getCustomToolRepository() {
411

412
    if (this.customToolRepository == null) {
3!
413
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
414
    }
415
    return this.customToolRepository;
3✔
416
  }
417

418
  @Override
419
  public Path getIdeHome() {
420

421
    return this.ideHome;
3✔
422
  }
423

424
  @Override
425
  public String getProjectName() {
426

427
    if (this.ideHome != null) {
3!
428
      return this.ideHome.getFileName().toString();
5✔
429
    }
430
    return "";
×
431
  }
432

433
  @Override
434
  public VersionIdentifier getProjectVersion() {
435

436
    if (this.ideHome != null) {
3!
437
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
438
      if (Files.exists(versionFile)) {
5✔
439
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
440
        return VersionIdentifier.of(version);
3✔
441
      }
442
    }
443
    return IdeMigrator.START_VERSION;
2✔
444
  }
445

446
  @Override
447
  public void setProjectVersion(VersionIdentifier version) {
448

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

457
  @Override
458
  public Path getIdeRoot() {
459

460
    return this.ideRoot;
3✔
461
  }
462

463
  @Override
464
  public Path getIdePath() {
465

466
    Path myIdeRoot = getIdeRoot();
3✔
467
    if (myIdeRoot == null) {
2!
468
      return null;
×
469
    }
470
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
471
  }
472

473
  @Override
474
  public Path getCwd() {
475

476
    return this.cwd;
3✔
477
  }
478

479
  @Override
480
  public Path getTempPath() {
481

482
    Path idePath = getIdePath();
3✔
483
    if (idePath == null) {
2!
484
      return null;
×
485
    }
486
    return idePath.resolve("tmp");
4✔
487
  }
488

489
  @Override
490
  public Path getTempDownloadPath() {
491

492
    Path tmp = getTempPath();
3✔
493
    if (tmp == null) {
2!
494
      return null;
×
495
    }
496
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
497
  }
498

499
  @Override
500
  public Path getUserHome() {
501

502
    return this.userHome;
3✔
503
  }
504

505
  /**
506
   * This method should only be used for tests to mock user home.
507
   *
508
   * @param userHome the new value of {@link #getUserHome()}.
509
   */
510
  protected void setUserHome(Path userHome) {
511

512
    this.userHome = userHome;
3✔
513
    resetPrivacyMap();
2✔
514
  }
1✔
515

516
  @Override
517
  public Path getUserHomeIde() {
518

519
    return this.userHomeIde;
3✔
520
  }
521

522
  @Override
523
  public Path getSettingsPath() {
524

525
    return this.settingsPath;
3✔
526
  }
527

528
  @Override
529
  public Path getSettingsGitRepository() {
530

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

540
  @Override
541
  public boolean isSettingsRepositorySymlinkOrJunction() {
542

543
    Path settingsPath = getSettingsPath();
3✔
544
    if (settingsPath == null) {
2!
545
      return false;
×
546
    }
547
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
548
  }
549

550
  @Override
551
  public Path getSettingsCommitIdPath() {
552

553
    return this.settingsCommitIdPath;
3✔
554
  }
555

556
  @Override
557
  public Path getConfPath() {
558

559
    return this.confPath;
3✔
560
  }
561

562
  @Override
563
  public Path getSoftwarePath() {
564

565
    if (this.ideHome == null) {
3✔
566
      return null;
2✔
567
    }
568
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
569
  }
570

571
  @Override
572
  public Path getSoftwareExtraPath() {
573

574
    Path softwarePath = getSoftwarePath();
3✔
575
    if (softwarePath == null) {
2!
576
      return null;
×
577
    }
578
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
579
  }
580

581
  @Override
582
  public Path getSoftwareRepositoryPath() {
583

584
    Path idePath = getIdePath();
3✔
585
    if (idePath == null) {
2!
586
      return null;
×
587
    }
588
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
589
  }
590

591
  @Override
592
  public Path getPluginsPath() {
593

594
    return this.pluginsPath;
3✔
595
  }
596

597
  @Override
598
  public String getWorkspaceName() {
599

600
    return this.workspaceName;
3✔
601
  }
602

603
  @Override
604
  public Path getWorkspacePath() {
605

606
    return this.workspacePath;
3✔
607
  }
608

609
  @Override
610
  public Path getDownloadPath() {
611

612
    return this.downloadPath;
3✔
613
  }
614

615
  @Override
616
  public Path getUrlsPath() {
617

618
    Path idePath = getIdePath();
3✔
619
    if (idePath == null) {
2!
620
      return null;
×
621
    }
622
    return idePath.resolve(FOLDER_URLS);
4✔
623
  }
624

625
  @Override
626
  public Path getToolRepositoryPath() {
627

628
    Path idePath = getIdePath();
3✔
629
    if (idePath == null) {
2!
630
      return null;
×
631
    }
632
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
633
  }
634

635
  @Override
636
  public SystemPath getPath() {
637

638
    return this.path;
3✔
639
  }
640

641
  @Override
642
  public EnvironmentVariables getVariables() {
643

644
    if (this.variables == null) {
3✔
645
      this.variables = createVariables();
4✔
646
    }
647
    return this.variables;
3✔
648
  }
649

650
  @Override
651
  public UrlMetadata getUrls() {
652

653
    if (this.urlMetadata == null) {
3✔
654
      if (!isTest()) {
3!
655
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
656
      }
657
      this.urlMetadata = new UrlMetadata(this);
6✔
658
    }
659
    return this.urlMetadata;
3✔
660
  }
661

662
  @Override
663
  public boolean isQuietMode() {
664

665
    return this.startContext.isQuietMode();
4✔
666
  }
667

668
  @Override
669
  public boolean isBatchMode() {
670

671
    return this.startContext.isBatchMode();
4✔
672
  }
673

674
  @Override
675
  public boolean isForceMode() {
676

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

680
  @Override
681
  public boolean isForcePull() {
682

683
    return this.startContext.isForcePull();
×
684
  }
685

686
  @Override
687
  public boolean isForcePlugins() {
688

689
    return this.startContext.isForcePlugins();
×
690
  }
691

692
  @Override
693
  public boolean isForceRepositories() {
694

695
    return this.startContext.isForceRepositories();
×
696
  }
697

698
  @Override
699
  public boolean isOfflineMode() {
700

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

704
  @Override
705
  public boolean isPrivacyMode() {
706
    return this.startContext.isPrivacyMode();
4✔
707
  }
708

709
  @Override
710
  public boolean isSkipUpdatesMode() {
711

712
    return this.startContext.isSkipUpdatesMode();
4✔
713
  }
714

715
  @Override
716
  public boolean isOnline() {
717

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

741
  private void configureNetworkProxy() {
742

743
    if (this.networkProxy == null) {
3✔
744
      this.networkProxy = new NetworkProxy(this);
6✔
745
      this.networkProxy.configure();
3✔
746
    }
747
  }
1✔
748

749
  @Override
750
  public Locale getLocale() {
751

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

759
  @Override
760
  public DirectoryMerger getWorkspaceMerger() {
761

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

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

774
    return this.defaultExecutionDirectory;
×
775
  }
776

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

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

787
  @Override
788
  public GitContext getGitContext() {
789

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

793
  @Override
794
  public ProcessContext newProcess() {
795

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

803
  @Override
804
  public IdeSystem getSystem() {
805

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

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

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

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

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

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

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

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

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

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

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

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

887

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

891
    if (!message.isBlank()) {
3!
892
      info(message);
3✔
893
    }
894
    if (isBatchMode()) {
3!
895
      if (isForceMode() || isForcePull()) {
×
896
        return defaultValue;
×
897
      } else {
898
        throw new CliAbortException();
×
899
      }
900
    }
901
    String input = readLine().trim();
4✔
902
    return input.isEmpty() ? defaultValue : input;
5!
903
  }
904

905
  @Override
906
  public String askForInput(String message) {
907

908
    String input;
909
    do {
910
      info(message);
3✔
911
      input = readLine().trim();
4✔
912
    } while (input.isEmpty());
3!
913

914
    return input;
2✔
915
  }
916

917
  @SuppressWarnings("unchecked")
918
  @Override
919
  public <O> O question(O[] options, String question, Object... args) {
920

921
    assert (options.length >= 2);
5!
922
    interaction(question, args);
4✔
923
    return displayOptionsAndGetAnswer(options);
4✔
924
  }
925

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

959
  /**
960
   * @return the input from the end-user (e.g. read from the console).
961
   */
962
  protected abstract String readLine();
963

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

966
    O duplicate = mapping.put(key, option);
5✔
967
    if (duplicate != null) {
2!
968
      throw new IllegalArgumentException("Duplicated option " + key);
×
969
    }
970
  }
1✔
971

972
  @Override
973
  public Step getCurrentStep() {
974

975
    return this.currentStep;
×
976
  }
977

978
  @Override
979
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
980

981
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
982
    return this.currentStep;
3✔
983
  }
984

985
  /**
986
   * Internal method to end the running {@link Step}.
987
   *
988
   * @param step the current {@link Step} to end.
989
   */
990
  public void endStep(StepImpl step) {
991

992
    if (step == this.currentStep) {
4!
993
      this.currentStep = this.currentStep.getParent();
6✔
994
    } else {
995
      String currentStepName = "null";
×
996
      if (this.currentStep != null) {
×
997
        currentStepName = this.currentStep.getName();
×
998
      }
999
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1000
    }
1001
  }
1✔
1002

1003
  /**
1004
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1005
   *
1006
   * @param arguments the {@link CliArgument}.
1007
   * @return the return code of the execution.
1008
   */
1009
  public int run(CliArguments arguments) {
1010

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

1051
  @Override
1052
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1053

1054
    this.startContext.deactivateLogging(threshold);
4✔
1055
    lambda.run();
2✔
1056
    this.startContext.activateLogging();
3✔
1057
  }
1✔
1058

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

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

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

1123
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1124

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

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

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

1203
  /**
1204
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1205
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1206
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1207
   */
1208
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1209

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

1236
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1237

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

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

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

1348
  @Override
1349
  public String findBash() {
1350

1351
    String bash = "bash";
2✔
1352
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1353
      bash = findBashOnWindows();
×
1354
    }
1355

1356
    return bash;
2✔
1357
  }
1358

1359
  private String findBashOnWindows() {
1360

1361
    // Check if Git Bash exists in the default location
1362
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1363
    if (Files.exists(defaultPath)) {
×
1364
      return defaultPath.toString();
×
1365
    }
1366

1367
    // If not found in the default location, try the registry query
1368
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1369
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1370
    String regQueryResult;
1371
    for (String bashVariant : bashVariants) {
×
1372
      for (String registryKey : registryKeys) {
×
1373
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1374
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1375

1376
        try {
1377
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1378
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1379
            StringBuilder output = new StringBuilder();
×
1380
            String line;
1381

1382
            while ((line = reader.readLine()) != null) {
×
1383
              output.append(line);
×
1384
            }
1385

1386
            int exitCode = process.waitFor();
×
1387
            if (exitCode != 0) {
×
1388
              return null;
×
1389
            }
1390

1391
            regQueryResult = output.toString();
×
1392
            if (regQueryResult != null) {
×
1393
              int index = regQueryResult.indexOf("REG_SZ");
×
1394
              if (index != -1) {
×
1395
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1396
                return path + "\\bin\\bash.exe";
×
1397
              }
1398
            }
1399

1400
          }
×
1401
        } catch (Exception e) {
×
1402
          return null;
×
1403
        }
×
1404
      }
1405
    }
1406
    // no bash found
1407
    return null;
×
1408
  }
1409

1410
  @Override
1411
  public WindowsPathSyntax getPathSyntax() {
1412

1413
    return this.pathSyntax;
3✔
1414
  }
1415

1416
  /**
1417
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1418
   */
1419
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1420

1421
    this.pathSyntax = pathSyntax;
3✔
1422
  }
1✔
1423

1424
  /**
1425
   * @return the {@link IdeStartContextImpl}.
1426
   */
1427
  public IdeStartContextImpl getStartContext() {
1428

1429
    return startContext;
3✔
1430
  }
1431

1432
  /**
1433
   * @return the {@link WindowsHelper}.
1434
   */
1435
  public final WindowsHelper getWindowsHelper() {
1436

1437
    if (this.windowsHelper == null) {
3✔
1438
      this.windowsHelper = createWindowsHelper();
4✔
1439
    }
1440
    return this.windowsHelper;
3✔
1441
  }
1442

1443
  /**
1444
   * @return the new {@link WindowsHelper} instance.
1445
   */
1446
  protected WindowsHelper createWindowsHelper() {
1447

1448
    return new WindowsHelperImpl(this);
×
1449
  }
1450

1451
  /**
1452
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1453
   */
1454
  public void reload() {
1455

1456
    this.variables = null;
3✔
1457
    this.customToolRepository = null;
3✔
1458
  }
1✔
1459

1460
  @Override
1461
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1462

1463
    assert (Files.isDirectory(installationPath));
6!
1464
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1465
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1466
  }
1✔
1467

1468
  /**
1469
   * Gets the logging context.
1470
   *
1471
   * @return {@link IdeContext}.
1472
   */
1473
  public static IdeContext getLoggingContext() {
1474

1475
    return loggingContext;
2✔
1476
  }
1477

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