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

devonfw / IDEasy / 16875680464

11 Aug 2025 09:08AM UTC coverage: 69.072% (-0.3%) from 69.373%
16875680464

Pull #1017

github

web-flow
Merge 8ec3d1978 into 59172b2c4
Pull Request #1017: #404: enhance logging with custom slf4j bridge

3359 of 5292 branches covered (63.47%)

Branch coverage included in aggregate %.

8681 of 12139 relevant lines covered (71.51%)

3.14 hits per line

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

65.38
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();
4✔
684
  }
685

686
  @Override
687
  public boolean isForcePlugins() {
688

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

692
  @Override
693
  public boolean isForceRepositories() {
694

695
    return this.startContext.isForceRepositories();
4✔
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
    while (true) {
892
      if (!message.isBlank()) {
3!
893
        interaction(message);
3✔
894
      }
895
      if (isBatchMode()) {
3!
896
        if (isForceMode()) {
×
897
          return defaultValue;
×
898
        } else {
899
          throw new CliAbortException();
×
900
        }
901
      }
902
      String input = readLine().trim();
4✔
903
      if (!input.isEmpty()) {
3!
904
        return input;
2✔
905
      } else {
906
        if (defaultValue != null) {
×
907
          return defaultValue;
×
908
        }
909
      }
910
    }
×
911
  }
912

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

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

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

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

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

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

968
  @Override
969
  public Step getCurrentStep() {
970

971
    return this.currentStep;
×
972
  }
973

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

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

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

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

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

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

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

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

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

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

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

1119
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1120

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

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

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

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

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

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

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

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

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

1344
  @Override
1345
  public String findBash() {
1346

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

1352
    return bash;
2✔
1353
  }
1354

1355
  private String findBashOnWindows() {
1356

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

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

1372
        try {
1373
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1374
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1375
            StringBuilder output = new StringBuilder();
×
1376
            String line;
1377

1378
            while ((line = reader.readLine()) != null) {
×
1379
              output.append(line);
×
1380
            }
1381

1382
            int exitCode = process.waitFor();
×
1383
            if (exitCode != 0) {
×
1384
              return null;
×
1385
            }
1386

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

1396
          }
×
1397
        } catch (Exception e) {
×
1398
          return null;
×
1399
        }
×
1400
      }
1401
    }
1402
    // no bash found
1403
    return null;
×
1404
  }
1405

1406
  @Override
1407
  public WindowsPathSyntax getPathSyntax() {
1408

1409
    return this.pathSyntax;
3✔
1410
  }
1411

1412
  /**
1413
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1414
   */
1415
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1416

1417
    this.pathSyntax = pathSyntax;
3✔
1418
  }
1✔
1419

1420
  /**
1421
   * @return the {@link IdeStartContextImpl}.
1422
   */
1423
  public IdeStartContextImpl getStartContext() {
1424

1425
    return startContext;
3✔
1426
  }
1427

1428
  /**
1429
   * @return the {@link WindowsHelper}.
1430
   */
1431
  public final WindowsHelper getWindowsHelper() {
1432

1433
    if (this.windowsHelper == null) {
3✔
1434
      this.windowsHelper = createWindowsHelper();
4✔
1435
    }
1436
    return this.windowsHelper;
3✔
1437
  }
1438

1439
  /**
1440
   * @return the new {@link WindowsHelper} instance.
1441
   */
1442
  protected WindowsHelper createWindowsHelper() {
1443

1444
    return new WindowsHelperImpl(this);
×
1445
  }
1446

1447
  /**
1448
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1449
   */
1450
  public void reload() {
1451

1452
    this.variables = null;
3✔
1453
    this.customToolRepository = null;
3✔
1454
  }
1✔
1455

1456
  @Override
1457
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1458

1459
    assert (Files.isDirectory(installationPath));
6!
1460
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1461
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1462
  }
1✔
1463

1464
  /**
1465
   * Gets the logging context.
1466
   *
1467
   * @return {@link IdeContext}.
1468
   */
1469
  public static IdeContext getLoggingContext() {
1470

1471
    return loggingContext;
2✔
1472
  }
1473

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