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

devonfw / IDEasy / 15911906253

26 Jun 2025 08:33PM UTC coverage: 68.328% (+0.5%) from 67.796%
15911906253

push

github

web-flow
#1303: Add option to show GDPR compliant console output and suggest for status in bug template (#1396)

Co-authored-by: jan-vcapgemini <jan-vincent.hoelzle@capgemini.com>
Co-authored-by: jan-vcapgemini <59438728+jan-vcapgemini@users.noreply.github.com>

3259 of 5176 branches covered (62.96%)

Branch coverage included in aggregate %.

8298 of 11738 relevant lines covered (70.69%)

3.13 hits per line

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

62.6
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
  /**
150
   * The constructor.
151
   *
152
   * @param startContext the {@link IdeLogger}.
153
   * @param workingDirectory the optional {@link Path} to current working directory.
154
   */
155
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
156

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

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

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

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

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

222
  private Path findIdeRoot(Path ideHomePath) {
223

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

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

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

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

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

307
    this.path = computeSystemPath();
4✔
308
  }
1✔
309

310
  private String getMessageIdeHomeFound() {
311

312
    return "IDE environment variables have been set for " + this.ideHome + " in workspace " + this.workspaceName;
7✔
313
  }
314

315
  private String getMessageNotInsideIdeProject() {
316

317
    return "You are not inside an IDE project: " + this.cwd;
5✔
318
  }
319

320
  private String getMessageIdeRootNotFound() {
321

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

330
  /**
331
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
332
   */
333
  public boolean isTest() {
334

335
    return false;
×
336
  }
337

338
  protected SystemPath computeSystemPath() {
339

340
    return new SystemPath(this);
×
341
  }
342

343
  private boolean isIdeHome(Path dir) {
344

345
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
346
      return false;
2✔
347
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
348
      return false;
×
349
    }
350
    return true;
2✔
351
  }
352

353
  private EnvironmentVariables createVariables() {
354

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

363
  protected AbstractEnvironmentVariables createSystemVariables() {
364

365
    return EnvironmentVariables.ofSystem(this);
3✔
366
  }
367

368
  @Override
369
  public SystemInfo getSystemInfo() {
370

371
    return this.systemInfo;
3✔
372
  }
373

374
  @Override
375
  public FileAccess getFileAccess() {
376

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

382
  @Override
383
  public CommandletManager getCommandletManager() {
384

385
    return this.commandletManager;
3✔
386
  }
387

388
  @Override
389
  public ToolRepository getDefaultToolRepository() {
390

391
    return this.defaultToolRepository;
3✔
392
  }
393

394
  @Override
395
  public MavenRepository getMavenToolRepository() {
396

397
    return this.mavenRepository;
3✔
398
  }
399

400
  @Override
401
  public CustomToolRepository getCustomToolRepository() {
402

403
    if (this.customToolRepository == null) {
3!
404
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
405
    }
406
    return this.customToolRepository;
3✔
407
  }
408

409
  @Override
410
  public Path getIdeHome() {
411

412
    return this.ideHome;
3✔
413
  }
414

415
  @Override
416
  public String getProjectName() {
417

418
    if (this.ideHome != null) {
3!
419
      return this.ideHome.getFileName().toString();
5✔
420
    }
421
    return "";
×
422
  }
423

424
  @Override
425
  public VersionIdentifier getProjectVersion() {
426

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

437
  @Override
438
  public void setProjectVersion(VersionIdentifier version) {
439

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

448
  @Override
449
  public Path getIdeRoot() {
450

451
    return this.ideRoot;
3✔
452
  }
453

454
  @Override
455
  public Path getIdePath() {
456

457
    Path myIdeRoot = getIdeRoot();
3✔
458
    if (myIdeRoot == null) {
2!
459
      return null;
×
460
    }
461
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
462
  }
463

464
  @Override
465
  public Path getCwd() {
466

467
    return this.cwd;
3✔
468
  }
469

470
  @Override
471
  public Path getTempPath() {
472

473
    Path idePath = getIdePath();
3✔
474
    if (idePath == null) {
2!
475
      return null;
×
476
    }
477
    return idePath.resolve("tmp");
4✔
478
  }
479

480
  @Override
481
  public Path getTempDownloadPath() {
482

483
    Path tmp = getTempPath();
3✔
484
    if (tmp == null) {
2!
485
      return null;
×
486
    }
487
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
488
  }
489

490
  @Override
491
  public Path getUserHome() {
492

493
    return this.userHome;
3✔
494
  }
495

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

503
    this.userHome = userHome;
3✔
504
    resetPrivacyMap();
2✔
505
  }
1✔
506

507
  @Override
508
  public Path getUserHomeIde() {
509

510
    return this.userHomeIde;
3✔
511
  }
512

513
  @Override
514
  public Path getSettingsPath() {
515

516
    return this.settingsPath;
3✔
517
  }
518

519
  @Override
520
  public Path getSettingsGitRepository() {
521

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

531
  @Override
532
  public boolean isSettingsRepositorySymlinkOrJunction() {
533

534
    Path settingsPath = getSettingsPath();
3✔
535
    if (settingsPath == null) {
2!
536
      return false;
×
537
    }
538
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
539
  }
540

541
  @Override
542
  public Path getSettingsCommitIdPath() {
543

544
    return this.settingsCommitIdPath;
3✔
545
  }
546

547
  @Override
548
  public Path getConfPath() {
549

550
    return this.confPath;
3✔
551
  }
552

553
  @Override
554
  public Path getSoftwarePath() {
555

556
    if (this.ideHome == null) {
3✔
557
      return null;
2✔
558
    }
559
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
560
  }
561

562
  @Override
563
  public Path getSoftwareExtraPath() {
564

565
    Path softwarePath = getSoftwarePath();
3✔
566
    if (softwarePath == null) {
2!
567
      return null;
×
568
    }
569
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
570
  }
571

572
  @Override
573
  public Path getSoftwareRepositoryPath() {
574

575
    Path idePath = getIdePath();
3✔
576
    if (idePath == null) {
2!
577
      return null;
×
578
    }
579
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
580
  }
581

582
  @Override
583
  public Path getPluginsPath() {
584

585
    return this.pluginsPath;
3✔
586
  }
587

588
  @Override
589
  public String getWorkspaceName() {
590

591
    return this.workspaceName;
3✔
592
  }
593

594
  @Override
595
  public Path getWorkspacePath() {
596

597
    return this.workspacePath;
3✔
598
  }
599

600
  @Override
601
  public Path getDownloadPath() {
602

603
    return this.downloadPath;
3✔
604
  }
605

606
  @Override
607
  public Path getUrlsPath() {
608

609
    Path idePath = getIdePath();
3✔
610
    if (idePath == null) {
2!
611
      return null;
×
612
    }
613
    return idePath.resolve(FOLDER_URLS);
4✔
614
  }
615

616
  @Override
617
  public Path getToolRepositoryPath() {
618

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

626
  @Override
627
  public SystemPath getPath() {
628

629
    return this.path;
3✔
630
  }
631

632
  @Override
633
  public EnvironmentVariables getVariables() {
634

635
    if (this.variables == null) {
3✔
636
      this.variables = createVariables();
4✔
637
    }
638
    return this.variables;
3✔
639
  }
640

641
  @Override
642
  public UrlMetadata getUrls() {
643

644
    if (this.urlMetadata == null) {
3✔
645
      if (!isTest()) {
3!
646
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
647
      }
648
      this.urlMetadata = new UrlMetadata(this);
6✔
649
    }
650
    return this.urlMetadata;
3✔
651
  }
652

653
  @Override
654
  public boolean isQuietMode() {
655

656
    return this.startContext.isQuietMode();
4✔
657
  }
658

659
  @Override
660
  public boolean isBatchMode() {
661

662
    return this.startContext.isBatchMode();
4✔
663
  }
664

665
  @Override
666
  public boolean isForceMode() {
667

668
    return this.startContext.isForceMode();
4✔
669
  }
670

671
  @Override
672
  public boolean isForcePull() {
673

674
    return this.startContext.isForcePull();
×
675
  }
676

677
  @Override
678
  public boolean isForcePlugins() {
679

680
    return this.startContext.isForcePlugins();
×
681
  }
682

683
  @Override
684
  public boolean isForceRepositories() {
685

686
    return this.startContext.isForceRepositories();
×
687
  }
688

689
  @Override
690
  public boolean isOfflineMode() {
691

692
    return this.startContext.isOfflineMode();
4✔
693
  }
694

695
  @Override
696
  public boolean isPrivacyMode() {
697
    return this.startContext.isPrivacyMode();
4✔
698
  }
699

700
  @Override
701
  public boolean isSkipUpdatesMode() {
702

703
    return this.startContext.isSkipUpdatesMode();
4✔
704
  }
705

706
  @Override
707
  public boolean isOnline() {
708

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

732
  private void configureNetworkProxy() {
733

734
    if (this.networkProxy == null) {
3✔
735
      this.networkProxy = new NetworkProxy(this);
6✔
736
      this.networkProxy.configure();
3✔
737
    }
738
  }
1✔
739

740
  @Override
741
  public Locale getLocale() {
742

743
    Locale locale = this.startContext.getLocale();
4✔
744
    if (locale == null) {
2✔
745
      locale = Locale.getDefault();
2✔
746
    }
747
    return locale;
2✔
748
  }
749

750
  @Override
751
  public DirectoryMerger getWorkspaceMerger() {
752

753
    if (this.workspaceMerger == null) {
3✔
754
      this.workspaceMerger = new DirectoryMerger(this);
6✔
755
    }
756
    return this.workspaceMerger;
3✔
757
  }
758

759
  /**
760
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
761
   */
762
  @Override
763
  public Path getDefaultExecutionDirectory() {
764

765
    return this.defaultExecutionDirectory;
×
766
  }
767

768
  /**
769
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
770
   */
771
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
772

773
    if (defaultExecutionDirectory != null) {
×
774
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
775
    }
776
  }
×
777

778
  @Override
779
  public GitContext getGitContext() {
780

781
    return new GitContextImpl(this);
×
782
  }
783

784
  @Override
785
  public ProcessContext newProcess() {
786

787
    ProcessContext processContext = createProcessContext();
3✔
788
    if (this.defaultExecutionDirectory != null) {
3!
789
      processContext.directory(this.defaultExecutionDirectory);
×
790
    }
791
    return processContext;
2✔
792
  }
793

794
  @Override
795
  public IdeSystem getSystem() {
796

797
    if (this.system == null) {
×
798
      this.system = new IdeSystemImpl(this);
×
799
    }
800
    return this.system;
×
801
  }
802

803
  /**
804
   * @return a new instance of {@link ProcessContext}.
805
   * @see #newProcess()
806
   */
807
  protected ProcessContext createProcessContext() {
808

809
    return new ProcessContextImpl(this);
5✔
810
  }
811

812
  @Override
813
  public IdeSubLogger level(IdeLogLevel level) {
814

815
    return this.startContext.level(level);
5✔
816
  }
817

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

830
  @Override
831
  public String formatArgument(Object argument) {
832

833
    if (argument == null) {
2✔
834
      return null;
2✔
835
    }
836
    String result = argument.toString();
3✔
837
    if (isPrivacyMode()) {
3✔
838
      if (this.privacyMap.isEmpty()) {
4✔
839
        initializePrivacyMap(this.userHome, "~");
5✔
840
        this.privacyMap.put(getProjectName(), "project");
7✔
841
      }
842
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
12✔
843
        result = result.replace(entry.getKey(), entry.getValue());
9✔
844
      }
1✔
845
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
846
    }
847
    return result;
2✔
848
  }
849

850
  /**
851
   * @param path the sensitive {@link Path} to
852
   * @param replacement the replacement to mask the {@link Path} in log output.
853
   */
854
  protected void initializePrivacyMap(Path path, String replacement) {
855

856
    if (path == null) {
2!
857
      return;
×
858
    }
859
    if (this.systemInfo.isWindows()) {
4!
860
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
861
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
862
    } else {
863
      this.privacyMap.put(path.toString(), replacement);
7✔
864
    }
865
  }
1✔
866

867
  /**
868
   * Resets the privacy map in case fundamental values have changed.
869
   */
870
  private void resetPrivacyMap() {
871

872
    this.privacyMap.clear();
3✔
873
  }
1✔
874

875

876
  @Override
877
  public String askForInput(String message, String defaultValue) {
878

879
    if (!message.isBlank()) {
×
880
      info(message);
×
881
    }
882
    if (isBatchMode()) {
×
883
      if (isForceMode() || isForcePull()) {
×
884
        return defaultValue;
×
885
      } else {
886
        throw new CliAbortException();
×
887
      }
888
    }
889
    String input = readLine().trim();
×
890
    return input.isEmpty() ? defaultValue : input;
×
891
  }
892

893
  @Override
894
  public String askForInput(String message) {
895

896
    String input;
897
    do {
898
      info(message);
3✔
899
      input = readLine().trim();
4✔
900
    } while (input.isEmpty());
3!
901

902
    return input;
2✔
903
  }
904

905
  @SuppressWarnings("unchecked")
906
  @Override
907
  public <O> O question(String question, O... options) {
908

909
    assert (options.length >= 2);
×
910
    interaction(question);
×
911
    Map<String, O> mapping = new HashMap<>(options.length);
×
912
    int i = 0;
×
913
    for (O option : options) {
×
914
      i++;
×
915
      String key = "" + option;
×
916
      addMapping(mapping, key, option);
×
917
      String numericKey = Integer.toString(i);
×
918
      if (numericKey.equals(key)) {
×
919
        trace("Options should not be numeric: " + key);
×
920
      } else {
921
        addMapping(mapping, numericKey, option);
×
922
      }
923
      interaction("Option " + numericKey + ": " + key);
×
924
    }
925
    O option = null;
×
926
    if (isBatchMode()) {
×
927
      if (isForceMode() || isForcePull()) {
×
928
        option = options[0];
×
929
        interaction("" + option);
×
930
      }
931
    } else {
932
      while (option == null) {
×
933
        String answer = readLine();
×
934
        option = mapping.get(answer);
×
935
        if (option == null) {
×
936
          warning("Invalid answer: '" + answer + "' - please try again.");
×
937
        }
938
      }
×
939
    }
940
    return option;
×
941
  }
942

943
  /**
944
   * @return the input from the end-user (e.g. read from the console).
945
   */
946
  protected abstract String readLine();
947

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

950
    O duplicate = mapping.put(key, option);
×
951
    if (duplicate != null) {
×
952
      throw new IllegalArgumentException("Duplicated option " + key);
×
953
    }
954
  }
×
955

956
  @Override
957
  public Step getCurrentStep() {
958

959
    return this.currentStep;
×
960
  }
961

962
  @Override
963
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
964

965
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
966
    return this.currentStep;
3✔
967
  }
968

969
  /**
970
   * Internal method to end the running {@link Step}.
971
   *
972
   * @param step the current {@link Step} to end.
973
   */
974
  public void endStep(StepImpl step) {
975

976
    if (step == this.currentStep) {
4!
977
      this.currentStep = this.currentStep.getParent();
6✔
978
    } else {
979
      String currentStepName = "null";
×
980
      if (this.currentStep != null) {
×
981
        currentStepName = this.currentStep.getName();
×
982
      }
983
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
984
    }
985
  }
1✔
986

987
  /**
988
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
989
   *
990
   * @param arguments the {@link CliArgument}.
991
   * @return the return code of the execution.
992
   */
993
  public int run(CliArguments arguments) {
994

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

1035
  @Override
1036
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1037

1038
    this.startContext.deactivateLogging(threshold);
4✔
1039
    lambda.run();
2✔
1040
    this.startContext.activateLogging();
3✔
1041
  }
1✔
1042

1043
  /**
1044
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1045
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1046
   *     {@link Commandlet} did not match and we have to try a different candidate).
1047
   */
1048
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1049

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

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

1107
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1108

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

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

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

1187
  /**
1188
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1189
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1190
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1191
   */
1192
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1193

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

1220
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1221

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

1278
  /**
1279
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1280
   *     {@link CliArguments#copy() copy} as needed.
1281
   * @param cmd the potential {@link Commandlet} to match.
1282
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1283
   */
1284
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1285

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

1332
  @Override
1333
  public String findBash() {
1334

1335
    String bash = "bash";
2✔
1336
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1337
      bash = findBashOnWindows();
×
1338
    }
1339

1340
    return bash;
2✔
1341
  }
1342

1343
  private String findBashOnWindows() {
1344

1345
    // Check if Git Bash exists in the default location
1346
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1347
    if (Files.exists(defaultPath)) {
×
1348
      return defaultPath.toString();
×
1349
    }
1350

1351
    // If not found in the default location, try the registry query
1352
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1353
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1354
    String regQueryResult;
1355
    for (String bashVariant : bashVariants) {
×
1356
      for (String registryKey : registryKeys) {
×
1357
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1358
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1359

1360
        try {
1361
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1362
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1363
            StringBuilder output = new StringBuilder();
×
1364
            String line;
1365

1366
            while ((line = reader.readLine()) != null) {
×
1367
              output.append(line);
×
1368
            }
1369

1370
            int exitCode = process.waitFor();
×
1371
            if (exitCode != 0) {
×
1372
              return null;
×
1373
            }
1374

1375
            regQueryResult = output.toString();
×
1376
            if (regQueryResult != null) {
×
1377
              int index = regQueryResult.indexOf("REG_SZ");
×
1378
              if (index != -1) {
×
1379
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1380
                return path + "\\bin\\bash.exe";
×
1381
              }
1382
            }
1383

1384
          }
×
1385
        } catch (Exception e) {
×
1386
          return null;
×
1387
        }
×
1388
      }
1389
    }
1390
    // no bash found
1391
    return null;
×
1392
  }
1393

1394
  @Override
1395
  public WindowsPathSyntax getPathSyntax() {
1396

1397
    return this.pathSyntax;
3✔
1398
  }
1399

1400
  /**
1401
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1402
   */
1403
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1404

1405
    this.pathSyntax = pathSyntax;
3✔
1406
  }
1✔
1407

1408
  /**
1409
   * @return the {@link IdeStartContextImpl}.
1410
   */
1411
  public IdeStartContextImpl getStartContext() {
1412

1413
    return startContext;
3✔
1414
  }
1415

1416
  /**
1417
   * @return the {@link WindowsHelper}.
1418
   */
1419
  public final WindowsHelper getWindowsHelper() {
1420

1421
    if (this.windowsHelper == null) {
3✔
1422
      this.windowsHelper = createWindowsHelper();
4✔
1423
    }
1424
    return this.windowsHelper;
3✔
1425
  }
1426

1427
  /**
1428
   * @return the new {@link WindowsHelper} instance.
1429
   */
1430
  protected WindowsHelper createWindowsHelper() {
1431

1432
    return new WindowsHelperImpl(this);
×
1433
  }
1434

1435
  /**
1436
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1437
   */
1438
  public void reload() {
1439

1440
    this.variables = null;
3✔
1441
    this.customToolRepository = null;
3✔
1442
  }
1✔
1443

1444
  @Override
1445
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1446

1447
    assert (Files.isDirectory(installationPath));
6!
1448
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1449
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1450
  }
1✔
1451

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