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

devonfw / IDEasy / 15921626796

27 Jun 2025 08:18AM UTC coverage: 68.252% (-0.08%) from 68.333%
15921626796

push

github

web-flow
#1303: fix critical bug on early logging causing mess (#1398)

3260 of 5184 branches covered (62.89%)

Branch coverage included in aggregate %.

8293 of 11743 relevant lines covered (70.62%)

3.13 hits per line

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

60.93
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
    resetPrivacyMap();
2✔
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.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
839
        initializePrivacyMap(this.userHome, "~");
×
840
        String projectName = getProjectName();
×
841
        if (!projectName.isEmpty()) {
×
842
          this.privacyMap.put(projectName, "project");
×
843
        }
844
      }
845
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
846
        result = result.replace(entry.getKey(), entry.getValue());
×
847
      }
×
848
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
849
    }
850
    return result;
2✔
851
  }
852

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

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

870
  /**
871
   * Resets the privacy map in case fundamental values have changed.
872
   */
873
  private void resetPrivacyMap() {
874

875
    this.privacyMap.clear();
3✔
876
  }
1✔
877

878

879
  @Override
880
  public String askForInput(String message, String defaultValue) {
881

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

896
  @Override
897
  public String askForInput(String message) {
898

899
    String input;
900
    do {
901
      info(message);
3✔
902
      input = readLine().trim();
4✔
903
    } while (input.isEmpty());
3!
904

905
    return input;
2✔
906
  }
907

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

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

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

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

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

959
  @Override
960
  public Step getCurrentStep() {
961

962
    return this.currentStep;
×
963
  }
964

965
  @Override
966
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
967

968
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
969
    return this.currentStep;
3✔
970
  }
971

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

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

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

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

1038
  @Override
1039
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1040

1041
    this.startContext.deactivateLogging(threshold);
4✔
1042
    lambda.run();
2✔
1043
    this.startContext.activateLogging();
3✔
1044
  }
1✔
1045

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

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

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

1110
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1111

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

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

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

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

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

1223
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1224

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

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

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

1335
  @Override
1336
  public String findBash() {
1337

1338
    String bash = "bash";
2✔
1339
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1340
      bash = findBashOnWindows();
×
1341
    }
1342

1343
    return bash;
2✔
1344
  }
1345

1346
  private String findBashOnWindows() {
1347

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

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

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

1369
            while ((line = reader.readLine()) != null) {
×
1370
              output.append(line);
×
1371
            }
1372

1373
            int exitCode = process.waitFor();
×
1374
            if (exitCode != 0) {
×
1375
              return null;
×
1376
            }
1377

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

1387
          }
×
1388
        } catch (Exception e) {
×
1389
          return null;
×
1390
        }
×
1391
      }
1392
    }
1393
    // no bash found
1394
    return null;
×
1395
  }
1396

1397
  @Override
1398
  public WindowsPathSyntax getPathSyntax() {
1399

1400
    return this.pathSyntax;
3✔
1401
  }
1402

1403
  /**
1404
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1405
   */
1406
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1407

1408
    this.pathSyntax = pathSyntax;
3✔
1409
  }
1✔
1410

1411
  /**
1412
   * @return the {@link IdeStartContextImpl}.
1413
   */
1414
  public IdeStartContextImpl getStartContext() {
1415

1416
    return startContext;
3✔
1417
  }
1418

1419
  /**
1420
   * @return the {@link WindowsHelper}.
1421
   */
1422
  public final WindowsHelper getWindowsHelper() {
1423

1424
    if (this.windowsHelper == null) {
3✔
1425
      this.windowsHelper = createWindowsHelper();
4✔
1426
    }
1427
    return this.windowsHelper;
3✔
1428
  }
1429

1430
  /**
1431
   * @return the new {@link WindowsHelper} instance.
1432
   */
1433
  protected WindowsHelper createWindowsHelper() {
1434

1435
    return new WindowsHelperImpl(this);
×
1436
  }
1437

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

1443
    this.variables = null;
3✔
1444
    this.customToolRepository = null;
3✔
1445
  }
1✔
1446

1447
  @Override
1448
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1449

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

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