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

devonfw / IDEasy / 17854514596

19 Sep 2025 09:34AM UTC coverage: 68.475% (-0.06%) from 68.539%
17854514596

push

github

web-flow
#907: add NpmRepository and further node/npm support as preparation for yarn and corepack (#1499)

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

3431 of 5487 branches covered (62.53%)

Branch coverage included in aggregate %.

8978 of 12635 relevant lines covered (71.06%)

3.12 hits per line

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

63.53
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.MvnRepository;
67
import com.devonfw.tools.ide.tool.repository.NpmRepository;
68
import com.devonfw.tools.ide.tool.repository.ToolRepository;
69
import com.devonfw.tools.ide.url.model.UrlMetadata;
70
import com.devonfw.tools.ide.util.DateTimeUtil;
71
import com.devonfw.tools.ide.util.PrivacyUtil;
72
import com.devonfw.tools.ide.validation.ValidationResult;
73
import com.devonfw.tools.ide.validation.ValidationResultValid;
74
import com.devonfw.tools.ide.validation.ValidationState;
75
import com.devonfw.tools.ide.variable.IdeVariables;
76
import com.devonfw.tools.ide.version.IdeVersion;
77
import com.devonfw.tools.ide.version.VersionIdentifier;
78

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

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

86
  private static final String LICENSE_URL = "https://github.com/devonfw/IDEasy/blob/main/documentation/LICENSE.adoc";
87
  public static final String BASH = "bash";
88

89
  private final IdeStartContextImpl startContext;
90

91
  private Path ideHome;
92

93
  private final Path ideRoot;
94

95
  private Path confPath;
96

97
  protected Path settingsPath;
98

99
  private Path settingsCommitIdPath;
100

101
  protected Path pluginsPath;
102

103
  private Path workspacePath;
104

105
  private String workspaceName;
106

107
  private Path cwd;
108

109
  private Path downloadPath;
110

111
  private Path userHome;
112

113
  private Path userHomeIde;
114

115
  private SystemPath path;
116

117
  private WindowsPathSyntax pathSyntax;
118

119
  private final SystemInfo systemInfo;
120

121
  private EnvironmentVariables variables;
122

123
  private final FileAccess fileAccess;
124

125
  protected CommandletManager commandletManager;
126

127
  protected ToolRepository defaultToolRepository;
128

129
  private CustomToolRepository customToolRepository;
130

131
  private final MvnRepository mvnRepository;
132

133
  private final NpmRepository npmRepository;
134

135
  private DirectoryMerger workspaceMerger;
136

137
  protected UrlMetadata urlMetadata;
138

139
  protected Path defaultExecutionDirectory;
140

141
  private StepImpl currentStep;
142

143
  protected Boolean online;
144

145
  protected IdeSystem system;
146

147
  private NetworkProxy networkProxy;
148

149
  private WindowsHelper windowsHelper;
150

151
  private final Map<String, String> privacyMap;
152

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

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

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

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

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

222
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
223
    this.mvnRepository = new MvnRepository(this);
6✔
224
    this.npmRepository = new NpmRepository(this);
6✔
225
  }
1✔
226

227
  private Path findIdeRoot(Path ideHomePath) {
228

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

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

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

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

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

315
  private String getMessageIdeHomeFound() {
316

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

324
  private String getMessageNotInsideIdeProject() {
325

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

329
  private String getMessageIdeRootNotFound() {
330

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

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

345
    return false;
×
346
  }
347

348
  protected SystemPath computeSystemPath() {
349

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

353
  private boolean isIdeHome(Path dir) {
354

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

363
  private EnvironmentVariables createVariables() {
364

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

373
  protected AbstractEnvironmentVariables createSystemVariables() {
374

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

378
  @Override
379
  public SystemInfo getSystemInfo() {
380

381
    return this.systemInfo;
3✔
382
  }
383

384
  @Override
385
  public FileAccess getFileAccess() {
386

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

392
  @Override
393
  public CommandletManager getCommandletManager() {
394

395
    return this.commandletManager;
3✔
396
  }
397

398
  @Override
399
  public ToolRepository getDefaultToolRepository() {
400

401
    return this.defaultToolRepository;
3✔
402
  }
403

404
  @Override
405
  public MvnRepository getMvnRepository() {
406

407
    return this.mvnRepository;
3✔
408
  }
409

410
  @Override
411
  public NpmRepository getNpmRepository() {
412

413
    return this.npmRepository;
3✔
414
  }
415

416
  @Override
417
  public CustomToolRepository getCustomToolRepository() {
418

419
    if (this.customToolRepository == null) {
3!
420
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
421
    }
422
    return this.customToolRepository;
3✔
423
  }
424

425
  @Override
426
  public Path getIdeHome() {
427

428
    return this.ideHome;
3✔
429
  }
430

431
  @Override
432
  public String getProjectName() {
433

434
    if (this.ideHome != null) {
3!
435
      return this.ideHome.getFileName().toString();
5✔
436
    }
437
    return "";
×
438
  }
439

440
  @Override
441
  public VersionIdentifier getProjectVersion() {
442

443
    if (this.ideHome != null) {
3!
444
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
445
      if (Files.exists(versionFile)) {
5✔
446
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
447
        return VersionIdentifier.of(version);
3✔
448
      }
449
    }
450
    return IdeMigrator.START_VERSION;
2✔
451
  }
452

453
  @Override
454
  public void setProjectVersion(VersionIdentifier version) {
455

456
    if (this.ideHome == null) {
3!
457
      throw new IllegalStateException("IDE_HOME not available!");
×
458
    }
459
    Objects.requireNonNull(version);
3✔
460
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
461
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
462
  }
1✔
463

464
  @Override
465
  public Path getIdeRoot() {
466

467
    return this.ideRoot;
3✔
468
  }
469

470
  @Override
471
  public Path getIdePath() {
472

473
    Path myIdeRoot = getIdeRoot();
3✔
474
    if (myIdeRoot == null) {
2!
475
      return null;
×
476
    }
477
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
478
  }
479

480
  @Override
481
  public Path getCwd() {
482

483
    return this.cwd;
3✔
484
  }
485

486
  @Override
487
  public Path getTempPath() {
488

489
    Path idePath = getIdePath();
3✔
490
    if (idePath == null) {
2!
491
      return null;
×
492
    }
493
    return idePath.resolve("tmp");
4✔
494
  }
495

496
  @Override
497
  public Path getTempDownloadPath() {
498

499
    Path tmp = getTempPath();
3✔
500
    if (tmp == null) {
2!
501
      return null;
×
502
    }
503
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
504
  }
505

506
  @Override
507
  public Path getUserHome() {
508

509
    return this.userHome;
3✔
510
  }
511

512
  /**
513
   * This method should only be used for tests to mock user home.
514
   *
515
   * @param userHome the new value of {@link #getUserHome()}.
516
   */
517
  protected void setUserHome(Path userHome) {
518

519
    this.userHome = userHome;
3✔
520
    resetPrivacyMap();
2✔
521
  }
1✔
522

523
  @Override
524
  public Path getUserHomeIde() {
525

526
    return this.userHomeIde;
3✔
527
  }
528

529
  @Override
530
  public Path getSettingsPath() {
531

532
    return this.settingsPath;
3✔
533
  }
534

535
  @Override
536
  public Path getSettingsGitRepository() {
537

538
    Path settingsPath = getSettingsPath();
3✔
539
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
540
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
541
      error("Settings repository exists but is not a git repository.");
3✔
542
      return null;
2✔
543
    }
544
    return settingsPath;
2✔
545
  }
546

547
  @Override
548
  public boolean isSettingsRepositorySymlinkOrJunction() {
549

550
    Path settingsPath = getSettingsPath();
3✔
551
    if (settingsPath == null) {
2!
552
      return false;
×
553
    }
554
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
555
  }
556

557
  @Override
558
  public Path getSettingsCommitIdPath() {
559

560
    return this.settingsCommitIdPath;
3✔
561
  }
562

563
  @Override
564
  public Path getConfPath() {
565

566
    return this.confPath;
3✔
567
  }
568

569
  @Override
570
  public Path getSoftwarePath() {
571

572
    if (this.ideHome == null) {
3✔
573
      return null;
2✔
574
    }
575
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
576
  }
577

578
  @Override
579
  public Path getSoftwareExtraPath() {
580

581
    Path softwarePath = getSoftwarePath();
3✔
582
    if (softwarePath == null) {
2!
583
      return null;
×
584
    }
585
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
586
  }
587

588
  @Override
589
  public Path getSoftwareRepositoryPath() {
590

591
    Path idePath = getIdePath();
3✔
592
    if (idePath == null) {
2!
593
      return null;
×
594
    }
595
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
596
  }
597

598
  @Override
599
  public Path getPluginsPath() {
600

601
    return this.pluginsPath;
3✔
602
  }
603

604
  @Override
605
  public String getWorkspaceName() {
606

607
    return this.workspaceName;
3✔
608
  }
609

610
  @Override
611
  public Path getWorkspacePath() {
612

613
    return this.workspacePath;
3✔
614
  }
615

616
  @Override
617
  public Path getDownloadPath() {
618

619
    return this.downloadPath;
3✔
620
  }
621

622
  @Override
623
  public Path getUrlsPath() {
624

625
    Path idePath = getIdePath();
3✔
626
    if (idePath == null) {
2!
627
      return null;
×
628
    }
629
    return idePath.resolve(FOLDER_URLS);
4✔
630
  }
631

632
  @Override
633
  public Path getToolRepositoryPath() {
634

635
    Path idePath = getIdePath();
3✔
636
    if (idePath == null) {
2!
637
      return null;
×
638
    }
639
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
640
  }
641

642
  @Override
643
  public SystemPath getPath() {
644

645
    return this.path;
3✔
646
  }
647

648
  @Override
649
  public EnvironmentVariables getVariables() {
650

651
    if (this.variables == null) {
3✔
652
      this.variables = createVariables();
4✔
653
    }
654
    return this.variables;
3✔
655
  }
656

657
  @Override
658
  public UrlMetadata getUrls() {
659

660
    if (this.urlMetadata == null) {
3✔
661
      if (!isTest()) {
3!
662
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
663
      }
664
      this.urlMetadata = new UrlMetadata(this);
6✔
665
    }
666
    return this.urlMetadata;
3✔
667
  }
668

669
  @Override
670
  public boolean isQuietMode() {
671

672
    return this.startContext.isQuietMode();
4✔
673
  }
674

675
  @Override
676
  public boolean isBatchMode() {
677

678
    return this.startContext.isBatchMode();
4✔
679
  }
680

681
  @Override
682
  public boolean isForceMode() {
683

684
    return this.startContext.isForceMode();
4✔
685
  }
686

687
  @Override
688
  public boolean isForcePull() {
689

690
    return this.startContext.isForcePull();
4✔
691
  }
692

693
  @Override
694
  public boolean isForcePlugins() {
695

696
    return this.startContext.isForcePlugins();
4✔
697
  }
698

699
  @Override
700
  public boolean isForceRepositories() {
701

702
    return this.startContext.isForceRepositories();
4✔
703
  }
704

705
  @Override
706
  public boolean isOfflineMode() {
707

708
    return this.startContext.isOfflineMode();
4✔
709
  }
710

711
  @Override
712
  public boolean isPrivacyMode() {
713
    return this.startContext.isPrivacyMode();
4✔
714
  }
715

716
  @Override
717
  public boolean isSkipUpdatesMode() {
718

719
    return this.startContext.isSkipUpdatesMode();
4✔
720
  }
721

722
  @Override
723
  public boolean isNoColorsMode() {
724

725
    return this.startContext.isNoColorsMode();
×
726
  }
727

728
  @Override
729
  public boolean isOnline() {
730

731
    if (this.online == null) {
3✔
732
      configureNetworkProxy();
2✔
733
      // we currently assume we have only a CLI process that runs shortly
734
      // therefore we run this check only once to save resources when this method is called many times
735
      String url = "https://www.github.com";
2✔
736
      try {
737
        int timeout = 1000;
2✔
738
        //open a connection to github.com and try to retrieve data
739
        //getContent fails if there is no connection
740
        URLConnection connection = new URL(url).openConnection();
6✔
741
        connection.setConnectTimeout(timeout);
3✔
742
        connection.getContent();
3✔
743
        this.online = Boolean.TRUE;
3✔
744
      } catch (Exception e) {
×
745
        if (debug().isEnabled()) {
×
746
          debug().log(e, "Error when trying to connect to {}", url);
×
747
        }
748
        this.online = Boolean.FALSE;
×
749
      }
1✔
750
    }
751
    return this.online.booleanValue();
4✔
752
  }
753

754
  private void configureNetworkProxy() {
755

756
    if (this.networkProxy == null) {
3✔
757
      this.networkProxy = new NetworkProxy(this);
6✔
758
      this.networkProxy.configure();
3✔
759
    }
760
  }
1✔
761

762
  @Override
763
  public Locale getLocale() {
764

765
    Locale locale = this.startContext.getLocale();
4✔
766
    if (locale == null) {
2✔
767
      locale = Locale.getDefault();
2✔
768
    }
769
    return locale;
2✔
770
  }
771

772
  @Override
773
  public DirectoryMerger getWorkspaceMerger() {
774

775
    if (this.workspaceMerger == null) {
3✔
776
      this.workspaceMerger = new DirectoryMerger(this);
6✔
777
    }
778
    return this.workspaceMerger;
3✔
779
  }
780

781
  /**
782
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
783
   */
784
  @Override
785
  public Path getDefaultExecutionDirectory() {
786

787
    return this.defaultExecutionDirectory;
×
788
  }
789

790
  /**
791
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
792
   */
793
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
794

795
    if (defaultExecutionDirectory != null) {
×
796
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
797
    }
798
  }
×
799

800
  @Override
801
  public GitContext getGitContext() {
802

803
    return new GitContextImpl(this);
×
804
  }
805

806
  @Override
807
  public ProcessContext newProcess() {
808

809
    ProcessContext processContext = createProcessContext();
3✔
810
    if (this.defaultExecutionDirectory != null) {
3!
811
      processContext.directory(this.defaultExecutionDirectory);
×
812
    }
813
    return processContext;
2✔
814
  }
815

816
  @Override
817
  public IdeSystem getSystem() {
818

819
    if (this.system == null) {
×
820
      this.system = new IdeSystemImpl(this);
×
821
    }
822
    return this.system;
×
823
  }
824

825
  /**
826
   * @return a new instance of {@link ProcessContext}.
827
   * @see #newProcess()
828
   */
829
  protected ProcessContext createProcessContext() {
830

831
    return new ProcessContextImpl(this);
5✔
832
  }
833

834
  @Override
835
  public IdeSubLogger level(IdeLogLevel level) {
836

837
    return this.startContext.level(level);
5✔
838
  }
839

840
  @Override
841
  public void logIdeHomeAndRootStatus() {
842
    if (this.ideRoot != null) {
3!
843
      success("IDE_ROOT is set to {}", this.ideRoot);
×
844
    }
845
    if (this.ideHome == null) {
3✔
846
      warning(getMessageNotInsideIdeProject());
5✔
847
    } else {
848
      success("IDE_HOME is set to {}", this.ideHome);
10✔
849
    }
850
  }
1✔
851

852
  @Override
853
  public String formatArgument(Object argument) {
854

855
    if (argument == null) {
2✔
856
      return null;
2✔
857
    }
858
    String result = argument.toString();
3✔
859
    if (isPrivacyMode()) {
3✔
860
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
861
        initializePrivacyMap(this.userHome, "~");
×
862
        String projectName = getProjectName();
×
863
        if (!projectName.isEmpty()) {
×
864
          this.privacyMap.put(projectName, "project");
×
865
        }
866
      }
867
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
868
        result = result.replace(entry.getKey(), entry.getValue());
×
869
      }
×
870
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
871
    }
872
    return result;
2✔
873
  }
874

875
  /**
876
   * @param path the sensitive {@link Path} to
877
   * @param replacement the replacement to mask the {@link Path} in log output.
878
   */
879
  protected void initializePrivacyMap(Path path, String replacement) {
880

881
    if (path == null) {
×
882
      return;
×
883
    }
884
    if (this.systemInfo.isWindows()) {
×
885
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
886
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
887
    } else {
888
      this.privacyMap.put(path.toString(), replacement);
×
889
    }
890
  }
×
891

892
  /**
893
   * Resets the privacy map in case fundamental values have changed.
894
   */
895
  private void resetPrivacyMap() {
896

897
    this.privacyMap.clear();
3✔
898
  }
1✔
899

900

901
  @Override
902
  public String askForInput(String message, String defaultValue) {
903

904
    while (true) {
905
      if (!message.isBlank()) {
3!
906
        interaction(message);
3✔
907
      }
908
      if (isBatchMode()) {
3!
909
        if (isForceMode()) {
×
910
          return defaultValue;
×
911
        } else {
912
          throw new CliAbortException();
×
913
        }
914
      }
915
      String input = readLine().trim();
4✔
916
      if (!input.isEmpty()) {
3!
917
        return input;
2✔
918
      } else {
919
        if (defaultValue != null) {
×
920
          return defaultValue;
×
921
        }
922
      }
923
    }
×
924
  }
925

926
  @SuppressWarnings("unchecked")
927
  @Override
928
  public <O> O question(O[] options, String question, Object... args) {
929

930
    assert (options.length >= 2);
5!
931
    interaction(question, args);
4✔
932
    return displayOptionsAndGetAnswer(options);
4✔
933
  }
934

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

968
  /**
969
   * @return the input from the end-user (e.g. read from the console).
970
   */
971
  protected abstract String readLine();
972

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

975
    O duplicate = mapping.put(key, option);
5✔
976
    if (duplicate != null) {
2!
977
      throw new IllegalArgumentException("Duplicated option " + key);
×
978
    }
979
  }
1✔
980

981
  @Override
982
  public Step getCurrentStep() {
983

984
    return this.currentStep;
×
985
  }
986

987
  @Override
988
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
989

990
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
991
    return this.currentStep;
3✔
992
  }
993

994
  /**
995
   * Internal method to end the running {@link Step}.
996
   *
997
   * @param step the current {@link Step} to end.
998
   */
999
  public void endStep(StepImpl step) {
1000

1001
    if (step == this.currentStep) {
4!
1002
      this.currentStep = this.currentStep.getParent();
6✔
1003
    } else {
1004
      String currentStepName = "null";
×
1005
      if (this.currentStep != null) {
×
1006
        currentStepName = this.currentStep.getName();
×
1007
      }
1008
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1009
    }
1010
  }
1✔
1011

1012
  /**
1013
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1014
   *
1015
   * @param arguments the {@link CliArgument}.
1016
   * @return the return code of the execution.
1017
   */
1018
  public int run(CliArguments arguments) {
1019

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

1060
  @Override
1061
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1062

1063
    this.startContext.deactivateLogging(threshold);
4✔
1064
    lambda.run();
2✔
1065
    this.startContext.activateLogging();
3✔
1066
  }
1✔
1067

1068
  /**
1069
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1070
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1071
   *     {@link Commandlet} did not match and we have to try a different candidate).
1072
   */
1073
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1074

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

1109
              } else {
1110
                interaction(
×
1111
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
1112
              }
1113
            }
1114
          }
1115
        }
1116
        boolean success = ensureLicenseAgreement(cmd);
4✔
1117
        if (!success) {
2!
1118
          return ValidationResultValid.get();
×
1119
        }
1120
        cmd.run();
2✔
1121
      } finally {
1122
        if (previousLogLevel != null) {
2!
1123
          this.startContext.setLogLevel(previousLogLevel);
×
1124
        }
1125
      }
1✔
1126
    } else {
1127
      trace("Commandlet did not match");
×
1128
    }
1129
    return result;
2✔
1130
  }
1131

1132
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1133

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

1175
    sb.setLength(0);
×
1176
    LocalDateTime now = LocalDateTime.now();
×
1177
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1178
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1179
    try {
1180
      Files.writeString(licenseAgreement, sb);
×
1181
    } catch (Exception e) {
×
1182
      throw new RuntimeException("Failed to save license agreement!", e);
×
1183
    }
×
1184
    if (logLevelInfoDisabled) {
×
1185
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
1186
    }
1187
    if (logLevelInteractionDisabled) {
×
1188
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
1189
    }
1190
    return true;
×
1191
  }
1192

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

1212
  /**
1213
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1214
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1215
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1216
   */
1217
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1218

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

1245
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1246

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

1303
  /**
1304
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1305
   *     {@link CliArguments#copy() copy} as needed.
1306
   * @param cmd the potential {@link Commandlet} to match.
1307
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1308
   */
1309
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1310

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

1357
  @Override
1358
  public String findBash() {
1359

1360
    String bash = BASH;
2✔
1361
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1362
      bash = findBashOnWindows();
×
1363
      if (bash == null) {
×
1364
        String variable = IdeVariables.BASH_PATH.getName();
×
1365
        bash = getVariables().get(variable);
×
1366
        if (bash == null) {
×
1367
          trace("Bash not found. Trying to search on system PATH.");
×
1368
          variable = IdeVariables.PATH.getName();
×
1369
          Path plainBash = Path.of(BASH);
×
1370
          Path bashPath = getPath().findBinary(plainBash);
×
1371
          bash = bashPath.toAbsolutePath().toString();
×
1372
          if (bash.contains("AppData\\Local\\Microsoft\\WindowsApps")) {
×
1373
            warning("Only found windows fake bash that is not usable!");
×
1374
            bash = null;
×
1375
          }
1376
        }
1377
        if (bash == null) {
×
1378
          info("Could not find bash in Windows registry, using bash from {} as fallback: {}", variable, bash);
×
1379
        }
1380
      }
1381
    }
1382
    return bash;
2✔
1383
  }
1384

1385
  private String findBashOnWindows() {
1386

1387
    // Check if Git Bash exists in the default location
1388
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1389
    if (Files.exists(defaultPath)) {
×
1390
      return defaultPath.toString();
×
1391
    }
1392

1393
    // If not found in the default location, try the registry query
1394
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1395
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1396
    String regQueryResult;
1397
    for (String bashVariant : bashVariants) {
×
1398
      trace("Trying to find bash variant: {}", bashVariant);
×
1399
      for (String registryKey : registryKeys) {
×
1400
        trace("Trying to find bash from registry key: {}", registryKey);
×
1401
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1402
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1403

1404
        try {
1405
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1406
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1407
            StringBuilder output = new StringBuilder();
×
1408
            String line;
1409

1410
            while ((line = reader.readLine()) != null) {
×
1411
              output.append(line);
×
1412
            }
1413

1414
            int exitCode = process.waitFor();
×
1415
            if (exitCode != 0) {
×
1416
              warning("Query to windows registry for finding bash failed with exit code {}", exitCode);
×
1417
              return null;
×
1418
            }
1419

1420
            regQueryResult = output.toString();
×
1421
            trace("Result from windows registry was: {}", regQueryResult);
×
1422
            int index = regQueryResult.indexOf("REG_SZ");
×
1423
            if (index != -1) {
×
1424
              String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1425
              String bashPath = path + "\\bin\\bash.exe";
×
1426
              debug("Found bash at: {}", bashPath);
×
1427
              return bashPath;
×
1428
            }
1429
          }
×
1430
        } catch (Exception e) {
×
1431
          error(e, "Query to windows registry for finding bash failed!");
×
1432
          return null;
×
1433
        }
×
1434
      }
1435
    }
1436
    // no bash found
1437
    return null;
×
1438
  }
1439

1440
  @Override
1441
  public WindowsPathSyntax getPathSyntax() {
1442

1443
    return this.pathSyntax;
3✔
1444
  }
1445

1446
  /**
1447
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1448
   */
1449
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1450

1451
    this.pathSyntax = pathSyntax;
3✔
1452
  }
1✔
1453

1454
  /**
1455
   * @return the {@link IdeStartContextImpl}.
1456
   */
1457
  public IdeStartContextImpl getStartContext() {
1458

1459
    return startContext;
3✔
1460
  }
1461

1462
  /**
1463
   * @return the {@link WindowsHelper}.
1464
   */
1465
  public final WindowsHelper getWindowsHelper() {
1466

1467
    if (this.windowsHelper == null) {
3✔
1468
      this.windowsHelper = createWindowsHelper();
4✔
1469
    }
1470
    return this.windowsHelper;
3✔
1471
  }
1472

1473
  /**
1474
   * @return the new {@link WindowsHelper} instance.
1475
   */
1476
  protected WindowsHelper createWindowsHelper() {
1477

1478
    return new WindowsHelperImpl(this);
×
1479
  }
1480

1481
  /**
1482
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1483
   */
1484
  public void reload() {
1485

1486
    this.variables = null;
3✔
1487
    this.customToolRepository = null;
3✔
1488
  }
1✔
1489

1490
  @Override
1491
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1492

1493
    assert (Files.isDirectory(installationPath));
6!
1494
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1495
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1496
  }
1✔
1497

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