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

devonfw / IDEasy / 18680241255

21 Oct 2025 10:03AM UTC coverage: 68.407% (-0.1%) from 68.522%
18680241255

Pull #1529

github

web-flow
Merge 6c92cf30d into 03c8a307b
Pull Request #1529: #1521: Use wiremock for npm repository.

3457 of 5541 branches covered (62.39%)

Branch coverage included in aggregate %.

9045 of 12735 relevant lines covered (71.02%)

3.12 hits per line

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

63.73
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 MvnRepository mvnRepository;
132

133
  private 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
  }
1✔
224

225
  /**
226
   * @return a new {@link MvnRepository}
227
   */
228
  protected MvnRepository createMvnRepository() {
229
    return new MvnRepository(this);
5✔
230
  }
231

232
  /**
233
   * @return a new {@link NpmRepository}
234
   */
235
  protected NpmRepository createNpmRepository() {
236
    return new NpmRepository(this);
×
237
  }
238

239
  private Path findIdeRoot(Path ideHomePath) {
240

241
    Path ideRootPath = null;
2✔
242
    if (ideHomePath != null) {
2✔
243
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
244
      ideRootPath = ideHomePath.getParent();
3✔
245
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
8!
246
        warning(
12✔
247
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
248
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
249
            ideRootPathFromEnv,
250
            ideHomePath.getFileName(), ideRootPath);
6✔
251
      }
252
    } else if (!isTest()) {
4!
253
      ideRootPath = getIdeRootPathFromEnv(true);
×
254
    }
255
    return ideRootPath;
2✔
256
  }
257

258
  /**
259
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
260
   */
261
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
262

263
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
264
    if (root != null) {
×
265
      Path rootPath = Path.of(root);
×
266
      if (Files.isDirectory(rootPath)) {
×
267
        Path absoluteRootPath = getFileAccess().toCanonicalPath(rootPath);
×
268
        if (withSanityCheck) {
×
269
          int nameCount = rootPath.getNameCount();
×
270
          int absoluteNameCount = absoluteRootPath.getNameCount();
×
271
          int delta = absoluteNameCount - nameCount;
×
272
          if (delta >= 0) {
×
273
            for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) {
×
274
              String rootName = rootPath.getName(nameIndex).toString();
×
275
              String absoluteRootName = absoluteRootPath.getName(nameIndex + delta).toString();
×
276
              if (!rootName.equals(absoluteRootName)) {
×
277
                warning("IDE_ROOT is set to {} but was expanded to absolute path {} and does not match for segment {} and {} - fix your IDEasy installation!",
×
278
                    rootPath, absoluteRootPath, rootName, absoluteRootName);
279
                break;
×
280
              }
281
            }
282
          } else {
283
            warning("IDE_ROOT is set to {} but was expanded to a shorter absolute path {}", rootPath,
×
284
                absoluteRootPath);
285
          }
286
        }
287
        return absoluteRootPath;
×
288
      } else if (withSanityCheck) {
×
289
        warning("IDE_ROOT is set to {} that is not an existing directory - fix your IDEasy installation!", rootPath);
×
290
      }
291
    }
292
    return null;
×
293
  }
294

295
  @Override
296
  public void setCwd(Path userDir, String workspace, Path ideHome) {
297

298
    this.cwd = userDir;
3✔
299
    this.workspaceName = workspace;
3✔
300
    this.ideHome = ideHome;
3✔
301
    if (ideHome == null) {
2✔
302
      this.workspacePath = null;
3✔
303
      this.confPath = null;
3✔
304
      this.settingsPath = null;
3✔
305
      this.pluginsPath = null;
4✔
306
    } else {
307
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
308
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
309
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
310
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
311
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
312
    }
313
    if (isTest()) {
3!
314
      // only for testing...
315
      if (this.ideHome == null) {
3✔
316
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
317
      } else {
318
        this.userHome = this.ideHome.resolve("home");
6✔
319
      }
320
    }
321
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
322
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
323
    resetPrivacyMap();
2✔
324
    this.path = computeSystemPath();
4✔
325
  }
1✔
326

327
  private String getMessageIdeHomeFound() {
328

329
    String wks = this.workspaceName;
3✔
330
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
331
      wks = "*".repeat(wks.length());
×
332
    }
333
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
334
  }
335

336
  private String getMessageNotInsideIdeProject() {
337

338
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
339
  }
340

341
  private String getMessageIdeRootNotFound() {
342

343
    String root = getSystem().getEnv("IDE_ROOT");
5✔
344
    if (root == null) {
2!
345
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
346
    } else {
347
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
348
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
349
    }
350
  }
351

352
  /**
353
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
354
   */
355
  public boolean isTest() {
356

357
    return false;
×
358
  }
359

360
  protected SystemPath computeSystemPath() {
361

362
    return new SystemPath(this);
×
363
  }
364

365
  private boolean isIdeHome(Path dir) {
366

367
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
368
      return false;
2✔
369
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
370
      return false;
×
371
    }
372
    return true;
2✔
373
  }
374

375
  private EnvironmentVariables createVariables() {
376

377
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
378
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
379
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
380
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
381
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
382
    return conf.resolved();
3✔
383
  }
384

385
  protected AbstractEnvironmentVariables createSystemVariables() {
386

387
    return EnvironmentVariables.ofSystem(this);
3✔
388
  }
389

390
  @Override
391
  public SystemInfo getSystemInfo() {
392

393
    return this.systemInfo;
3✔
394
  }
395

396
  @Override
397
  public FileAccess getFileAccess() {
398

399
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
400
    configureNetworkProxy();
2✔
401
    return this.fileAccess;
3✔
402
  }
403

404
  @Override
405
  public CommandletManager getCommandletManager() {
406

407
    return this.commandletManager;
3✔
408
  }
409

410
  @Override
411
  public ToolRepository getDefaultToolRepository() {
412

413
    return this.defaultToolRepository;
3✔
414
  }
415

416
  @Override
417
  public MvnRepository getMvnRepository() {
418
    if (this.mvnRepository == null) {
3✔
419
      this.mvnRepository = createMvnRepository();
4✔
420
    }
421
    return this.mvnRepository;
3✔
422
  }
423

424
  @Override
425
  public NpmRepository getNpmRepository() {
426
    if (this.npmRepository == null) {
3✔
427
      this.npmRepository = createNpmRepository();
4✔
428
    }
429
    return this.npmRepository;
3✔
430
  }
431

432
  @Override
433
  public CustomToolRepository getCustomToolRepository() {
434

435
    if (this.customToolRepository == null) {
3!
436
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
437
    }
438
    return this.customToolRepository;
3✔
439
  }
440

441
  @Override
442
  public Path getIdeHome() {
443

444
    return this.ideHome;
3✔
445
  }
446

447
  @Override
448
  public String getProjectName() {
449

450
    if (this.ideHome != null) {
3!
451
      return this.ideHome.getFileName().toString();
5✔
452
    }
453
    return "";
×
454
  }
455

456
  @Override
457
  public VersionIdentifier getProjectVersion() {
458

459
    if (this.ideHome != null) {
3!
460
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
461
      if (Files.exists(versionFile)) {
5✔
462
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
463
        return VersionIdentifier.of(version);
3✔
464
      }
465
    }
466
    return IdeMigrator.START_VERSION;
2✔
467
  }
468

469
  @Override
470
  public void setProjectVersion(VersionIdentifier version) {
471

472
    if (this.ideHome == null) {
3!
473
      throw new IllegalStateException("IDE_HOME not available!");
×
474
    }
475
    Objects.requireNonNull(version);
3✔
476
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
477
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
478
  }
1✔
479

480
  @Override
481
  public Path getIdeRoot() {
482

483
    return this.ideRoot;
3✔
484
  }
485

486
  @Override
487
  public Path getIdePath() {
488

489
    Path myIdeRoot = getIdeRoot();
3✔
490
    if (myIdeRoot == null) {
2!
491
      return null;
×
492
    }
493
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
494
  }
495

496
  @Override
497
  public Path getCwd() {
498

499
    return this.cwd;
3✔
500
  }
501

502
  @Override
503
  public Path getTempPath() {
504

505
    Path idePath = getIdePath();
3✔
506
    if (idePath == null) {
2!
507
      return null;
×
508
    }
509
    return idePath.resolve("tmp");
4✔
510
  }
511

512
  @Override
513
  public Path getTempDownloadPath() {
514

515
    Path tmp = getTempPath();
3✔
516
    if (tmp == null) {
2!
517
      return null;
×
518
    }
519
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
520
  }
521

522
  @Override
523
  public Path getUserHome() {
524

525
    return this.userHome;
3✔
526
  }
527

528
  /**
529
   * This method should only be used for tests to mock user home.
530
   *
531
   * @param userHome the new value of {@link #getUserHome()}.
532
   */
533
  protected void setUserHome(Path userHome) {
534

535
    this.userHome = userHome;
3✔
536
    resetPrivacyMap();
2✔
537
  }
1✔
538

539
  @Override
540
  public Path getUserHomeIde() {
541

542
    return this.userHomeIde;
3✔
543
  }
544

545
  @Override
546
  public Path getSettingsPath() {
547

548
    return this.settingsPath;
3✔
549
  }
550

551
  @Override
552
  public Path getSettingsGitRepository() {
553

554
    Path settingsPath = getSettingsPath();
3✔
555
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
556
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
557
      error("Settings repository exists but is not a git repository.");
3✔
558
      return null;
2✔
559
    }
560
    return settingsPath;
2✔
561
  }
562

563
  @Override
564
  public boolean isSettingsRepositorySymlinkOrJunction() {
565

566
    Path settingsPath = getSettingsPath();
3✔
567
    if (settingsPath == null) {
2!
568
      return false;
×
569
    }
570
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
571
  }
572

573
  @Override
574
  public Path getSettingsCommitIdPath() {
575

576
    return this.settingsCommitIdPath;
3✔
577
  }
578

579
  @Override
580
  public Path getConfPath() {
581

582
    return this.confPath;
3✔
583
  }
584

585
  @Override
586
  public Path getSoftwarePath() {
587

588
    if (this.ideHome == null) {
3✔
589
      return null;
2✔
590
    }
591
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
592
  }
593

594
  @Override
595
  public Path getSoftwareExtraPath() {
596

597
    Path softwarePath = getSoftwarePath();
3✔
598
    if (softwarePath == null) {
2!
599
      return null;
×
600
    }
601
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
602
  }
603

604
  @Override
605
  public Path getSoftwareRepositoryPath() {
606

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

614
  @Override
615
  public Path getPluginsPath() {
616

617
    return this.pluginsPath;
3✔
618
  }
619

620
  @Override
621
  public String getWorkspaceName() {
622

623
    return this.workspaceName;
3✔
624
  }
625

626
  @Override
627
  public Path getWorkspacePath() {
628

629
    return this.workspacePath;
3✔
630
  }
631

632
  @Override
633
  public Path getDownloadPath() {
634

635
    return this.downloadPath;
3✔
636
  }
637

638
  @Override
639
  public Path getUrlsPath() {
640

641
    Path idePath = getIdePath();
3✔
642
    if (idePath == null) {
2!
643
      return null;
×
644
    }
645
    return idePath.resolve(FOLDER_URLS);
4✔
646
  }
647

648
  @Override
649
  public Path getToolRepositoryPath() {
650

651
    Path idePath = getIdePath();
3✔
652
    if (idePath == null) {
2!
653
      return null;
×
654
    }
655
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
656
  }
657

658
  @Override
659
  public SystemPath getPath() {
660

661
    return this.path;
3✔
662
  }
663

664
  @Override
665
  public EnvironmentVariables getVariables() {
666

667
    if (this.variables == null) {
3✔
668
      this.variables = createVariables();
4✔
669
    }
670
    return this.variables;
3✔
671
  }
672

673
  @Override
674
  public UrlMetadata getUrls() {
675

676
    if (this.urlMetadata == null) {
3✔
677
      if (!isTest()) {
3!
678
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
679
      }
680
      this.urlMetadata = new UrlMetadata(this);
6✔
681
    }
682
    return this.urlMetadata;
3✔
683
  }
684

685
  @Override
686
  public boolean isQuietMode() {
687

688
    return this.startContext.isQuietMode();
4✔
689
  }
690

691
  @Override
692
  public boolean isBatchMode() {
693

694
    return this.startContext.isBatchMode();
4✔
695
  }
696

697
  @Override
698
  public boolean isForceMode() {
699

700
    return this.startContext.isForceMode();
4✔
701
  }
702

703
  @Override
704
  public boolean isForcePull() {
705

706
    return this.startContext.isForcePull();
4✔
707
  }
708

709
  @Override
710
  public boolean isForcePlugins() {
711

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

715
  @Override
716
  public boolean isForceRepositories() {
717

718
    return this.startContext.isForceRepositories();
4✔
719
  }
720

721
  @Override
722
  public boolean isOfflineMode() {
723

724
    return this.startContext.isOfflineMode();
4✔
725
  }
726

727
  @Override
728
  public boolean isPrivacyMode() {
729
    return this.startContext.isPrivacyMode();
4✔
730
  }
731

732
  @Override
733
  public boolean isSkipUpdatesMode() {
734

735
    return this.startContext.isSkipUpdatesMode();
4✔
736
  }
737

738
  @Override
739
  public boolean isNoColorsMode() {
740

741
    return this.startContext.isNoColorsMode();
×
742
  }
743

744
  @Override
745
  public boolean isOnline() {
746

747
    if (this.online == null) {
3✔
748
      configureNetworkProxy();
2✔
749
      // we currently assume we have only a CLI process that runs shortly
750
      // therefore we run this check only once to save resources when this method is called many times
751
      String url = "https://www.github.com";
2✔
752
      try {
753
        int timeout = 1000;
2✔
754
        //open a connection to github.com and try to retrieve data
755
        //getContent fails if there is no connection
756
        URLConnection connection = new URL(url).openConnection();
6✔
757
        connection.setConnectTimeout(timeout);
3✔
758
        connection.getContent();
3✔
759
        this.online = Boolean.TRUE;
3✔
760
      } catch (Exception e) {
×
761
        if (debug().isEnabled()) {
×
762
          debug().log(e, "Error when trying to connect to {}", url);
×
763
        }
764
        this.online = Boolean.FALSE;
×
765
      }
1✔
766
    }
767
    return this.online.booleanValue();
4✔
768
  }
769

770
  private void configureNetworkProxy() {
771

772
    if (this.networkProxy == null) {
3✔
773
      this.networkProxy = new NetworkProxy(this);
6✔
774
      this.networkProxy.configure();
3✔
775
    }
776
  }
1✔
777

778
  @Override
779
  public Locale getLocale() {
780

781
    Locale locale = this.startContext.getLocale();
4✔
782
    if (locale == null) {
2✔
783
      locale = Locale.getDefault();
2✔
784
    }
785
    return locale;
2✔
786
  }
787

788
  @Override
789
  public DirectoryMerger getWorkspaceMerger() {
790

791
    if (this.workspaceMerger == null) {
3✔
792
      this.workspaceMerger = new DirectoryMerger(this);
6✔
793
    }
794
    return this.workspaceMerger;
3✔
795
  }
796

797
  /**
798
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
799
   */
800
  @Override
801
  public Path getDefaultExecutionDirectory() {
802

803
    return this.defaultExecutionDirectory;
×
804
  }
805

806
  /**
807
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
808
   */
809
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
810

811
    if (defaultExecutionDirectory != null) {
×
812
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
813
    }
814
  }
×
815

816
  @Override
817
  public GitContext getGitContext() {
818

819
    return new GitContextImpl(this);
×
820
  }
821

822
  @Override
823
  public ProcessContext newProcess() {
824

825
    ProcessContext processContext = createProcessContext();
3✔
826
    if (this.defaultExecutionDirectory != null) {
3!
827
      processContext.directory(this.defaultExecutionDirectory);
×
828
    }
829
    return processContext;
2✔
830
  }
831

832
  @Override
833
  public IdeSystem getSystem() {
834

835
    if (this.system == null) {
×
836
      this.system = new IdeSystemImpl(this);
×
837
    }
838
    return this.system;
×
839
  }
840

841
  /**
842
   * @return a new instance of {@link ProcessContext}.
843
   * @see #newProcess()
844
   */
845
  protected ProcessContext createProcessContext() {
846

847
    return new ProcessContextImpl(this);
5✔
848
  }
849

850
  @Override
851
  public IdeSubLogger level(IdeLogLevel level) {
852

853
    return this.startContext.level(level);
5✔
854
  }
855

856
  @Override
857
  public void logIdeHomeAndRootStatus() {
858
    if (this.ideRoot != null) {
3!
859
      success("IDE_ROOT is set to {}", this.ideRoot);
×
860
    }
861
    if (this.ideHome == null) {
3✔
862
      warning(getMessageNotInsideIdeProject());
5✔
863
    } else {
864
      success("IDE_HOME is set to {}", this.ideHome);
10✔
865
    }
866
  }
1✔
867

868
  @Override
869
  public String formatArgument(Object argument) {
870

871
    if (argument == null) {
2✔
872
      return null;
2✔
873
    }
874
    String result = argument.toString();
3✔
875
    if (isPrivacyMode()) {
3✔
876
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
877
        initializePrivacyMap(this.userHome, "~");
×
878
        String projectName = getProjectName();
×
879
        if (!projectName.isEmpty()) {
×
880
          this.privacyMap.put(projectName, "project");
×
881
        }
882
      }
883
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
884
        result = result.replace(entry.getKey(), entry.getValue());
×
885
      }
×
886
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
887
    }
888
    return result;
2✔
889
  }
890

891
  /**
892
   * @param path the sensitive {@link Path} to
893
   * @param replacement the replacement to mask the {@link Path} in log output.
894
   */
895
  protected void initializePrivacyMap(Path path, String replacement) {
896

897
    if (path == null) {
×
898
      return;
×
899
    }
900
    if (this.systemInfo.isWindows()) {
×
901
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
902
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
903
    } else {
904
      this.privacyMap.put(path.toString(), replacement);
×
905
    }
906
  }
×
907

908
  /**
909
   * Resets the privacy map in case fundamental values have changed.
910
   */
911
  private void resetPrivacyMap() {
912

913
    this.privacyMap.clear();
3✔
914
  }
1✔
915

916

917
  @Override
918
  public String askForInput(String message, String defaultValue) {
919

920
    while (true) {
921
      if (!message.isBlank()) {
3!
922
        interaction(message);
3✔
923
      }
924
      if (isBatchMode()) {
3!
925
        if (isForceMode()) {
×
926
          return defaultValue;
×
927
        } else {
928
          throw new CliAbortException();
×
929
        }
930
      }
931
      String input = readLine().trim();
4✔
932
      if (!input.isEmpty()) {
3!
933
        return input;
2✔
934
      } else {
935
        if (defaultValue != null) {
×
936
          return defaultValue;
×
937
        }
938
      }
939
    }
×
940
  }
941

942
  @SuppressWarnings("unchecked")
943
  @Override
944
  public <O> O question(O[] options, String question, Object... args) {
945

946
    assert (options.length >= 2);
5!
947
    interaction(question, args);
4✔
948
    return displayOptionsAndGetAnswer(options);
4✔
949
  }
950

951
  private <O> O displayOptionsAndGetAnswer(O[] options) {
952
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
953
    int i = 0;
2✔
954
    for (O option : options) {
16✔
955
      i++;
1✔
956
      String key = "" + option;
4✔
957
      addMapping(mapping, key, option);
4✔
958
      String numericKey = Integer.toString(i);
3✔
959
      if (numericKey.equals(key)) {
4!
960
        trace("Options should not be numeric: " + key);
×
961
      } else {
962
        addMapping(mapping, numericKey, option);
4✔
963
      }
964
      interaction("Option " + numericKey + ": " + key);
5✔
965
    }
966
    O option = null;
2✔
967
    if (isBatchMode()) {
3!
968
      if (isForceMode()) {
×
969
        option = options[0];
×
970
        interaction("" + option);
×
971
      }
972
    } else {
973
      while (option == null) {
2✔
974
        String answer = readLine();
3✔
975
        option = mapping.get(answer);
4✔
976
        if (option == null) {
2!
977
          warning("Invalid answer: '" + answer + "' - please try again.");
×
978
        }
979
      }
1✔
980
    }
981
    return option;
2✔
982
  }
983

984
  /**
985
   * @return the input from the end-user (e.g. read from the console).
986
   */
987
  protected abstract String readLine();
988

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

991
    O duplicate = mapping.put(key, option);
5✔
992
    if (duplicate != null) {
2!
993
      throw new IllegalArgumentException("Duplicated option " + key);
×
994
    }
995
  }
1✔
996

997
  @Override
998
  public Step getCurrentStep() {
999

1000
    return this.currentStep;
×
1001
  }
1002

1003
  @Override
1004
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1005

1006
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1007
    return this.currentStep;
3✔
1008
  }
1009

1010
  /**
1011
   * Internal method to end the running {@link Step}.
1012
   *
1013
   * @param step the current {@link Step} to end.
1014
   */
1015
  public void endStep(StepImpl step) {
1016

1017
    if (step == this.currentStep) {
4!
1018
      this.currentStep = this.currentStep.getParent();
6✔
1019
    } else {
1020
      String currentStepName = "null";
×
1021
      if (this.currentStep != null) {
×
1022
        currentStepName = this.currentStep.getName();
×
1023
      }
1024
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1025
    }
1026
  }
1✔
1027

1028
  /**
1029
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1030
   *
1031
   * @param arguments the {@link CliArgument}.
1032
   * @return the return code of the execution.
1033
   */
1034
  public int run(CliArguments arguments) {
1035

1036
    CliArgument current = arguments.current();
3✔
1037
    assert (this.currentStep == null);
4!
1038
    boolean supressStepSuccess = false;
2✔
1039
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1040
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1041
    Commandlet cmd = null;
2✔
1042
    ValidationResult result = null;
2✔
1043
    try {
1044
      while (commandletIterator.hasNext()) {
3✔
1045
        cmd = commandletIterator.next();
4✔
1046
        result = applyAndRun(arguments.copy(), cmd);
6✔
1047
        if (result.isValid()) {
3!
1048
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1049
          step.success();
2✔
1050
          return ProcessResult.SUCCESS;
4✔
1051
        }
1052
      }
1053
      this.startContext.activateLogging();
3✔
1054
      verifyIdeMinVersion(false);
3✔
1055
      if (result != null) {
2!
1056
        error(result.getErrorMessage());
×
1057
      }
1058
      step.error("Invalid arguments: {}", current.getArgs());
10✔
1059
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
1060
      if (cmd != null) {
2!
1061
        help.commandlet.setValue(cmd);
×
1062
      }
1063
      help.run();
2✔
1064
      return 1;
4✔
1065
    } catch (Throwable t) {
1✔
1066
      this.startContext.activateLogging();
3✔
1067
      step.error(t, true);
4✔
1068
      throw t;
2✔
1069
    } finally {
1070
      step.close();
2✔
1071
      assert (this.currentStep == null);
4!
1072
      step.logSummary(supressStepSuccess);
3✔
1073
    }
1074
  }
1075

1076
  @Override
1077
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1078

1079
    this.startContext.deactivateLogging(threshold);
4✔
1080
    lambda.run();
2✔
1081
    this.startContext.activateLogging();
3✔
1082
  }
1✔
1083

1084
  /**
1085
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1086
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1087
   *     {@link Commandlet} did not match and we have to try a different candidate).
1088
   */
1089
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1090

1091
    IdeLogLevel previousLogLevel = null;
2✔
1092
    cmd.reset();
2✔
1093
    ValidationResult result = apply(arguments, cmd);
5✔
1094
    if (result.isValid()) {
3!
1095
      result = cmd.validate();
3✔
1096
    }
1097
    if (result.isValid()) {
3!
1098
      debug("Running commandlet {}", cmd);
9✔
1099
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1100
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1101
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1102
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1103
      }
1104
      try {
1105
        if (cmd.isProcessableOutput()) {
3!
1106
          if (!debug().isEnabled()) {
×
1107
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1108
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
1109
          }
1110
          this.startContext.activateLogging();
×
1111
        } else {
1112
          this.startContext.activateLogging();
3✔
1113
          if (cmd.isIdeHomeRequired()) {
3!
1114
            debug(getMessageIdeHomeFound());
4✔
1115
          }
1116
          Path settingsRepository = getSettingsGitRepository();
3✔
1117
          if (settingsRepository != null) {
2!
1118
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1119
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1120
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1121
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1122
                interaction(
×
1123
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
1124

1125
              } else {
1126
                interaction(
×
1127
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
1128
              }
1129
            }
1130
          }
1131
        }
1132
        boolean success = ensureLicenseAgreement(cmd);
4✔
1133
        if (!success) {
2!
1134
          return ValidationResultValid.get();
×
1135
        }
1136
        cmd.run();
2✔
1137
      } finally {
1138
        if (previousLogLevel != null) {
2!
1139
          this.startContext.setLogLevel(previousLogLevel);
×
1140
        }
1141
      }
1✔
1142
    } else {
1143
      trace("Commandlet did not match");
×
1144
    }
1145
    return result;
2✔
1146
  }
1147

1148
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1149

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

1191
    sb.setLength(0);
×
1192
    LocalDateTime now = LocalDateTime.now();
×
1193
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1194
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1195
    try {
1196
      Files.writeString(licenseAgreement, sb);
×
1197
    } catch (Exception e) {
×
1198
      throw new RuntimeException("Failed to save license agreement!", e);
×
1199
    }
×
1200
    if (logLevelInfoDisabled) {
×
1201
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
1202
    }
1203
    if (logLevelInteractionDisabled) {
×
1204
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
1205
    }
1206
    return true;
×
1207
  }
1208

1209
  @Override
1210
  public void verifyIdeMinVersion(boolean throwException) {
1211
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1212
    if (minVersion == null) {
2✔
1213
      return;
1✔
1214
    }
1215
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1216
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1217
          + "However, this is too old as your project requires at latest version %s\n"
1218
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1219
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1220
      if (throwException) {
2✔
1221
        throw new CliException(message);
5✔
1222
      } else {
1223
        warning(message);
3✔
1224
      }
1225
    }
1226
  }
1✔
1227

1228
  /**
1229
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1230
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1231
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1232
   */
1233
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1234

1235
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1236
    if (arguments.current().isStart()) {
4✔
1237
      arguments.next();
3✔
1238
    }
1239
    if (includeContextOptions) {
2✔
1240
      ContextCommandlet cc = new ContextCommandlet();
4✔
1241
      for (Property<?> property : cc.getProperties()) {
11✔
1242
        assert (property.isOption());
4!
1243
        property.apply(arguments, this, cc, collector);
7✔
1244
      }
1✔
1245
    }
1246
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1247
    CliArgument current = arguments.current();
3✔
1248
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1249
      collector.add(current.get(), null, null, null);
7✔
1250
    }
1251
    arguments.next();
3✔
1252
    while (commandletIterator.hasNext()) {
3✔
1253
      Commandlet cmd = commandletIterator.next();
4✔
1254
      if (!arguments.current().isEnd()) {
4✔
1255
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1256
      }
1257
    }
1✔
1258
    return collector.getSortedCandidates();
3✔
1259
  }
1260

1261
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1262

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

1319
  /**
1320
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1321
   *     {@link CliArguments#copy() copy} as needed.
1322
   * @param cmd the potential {@link Commandlet} to match.
1323
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1324
   */
1325
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1326

1327
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1328
    CliArgument currentArgument = arguments.current();
3✔
1329
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1330
    Property<?> property = null;
2✔
1331
    if (propertyIterator.hasNext()) {
3!
1332
      property = propertyIterator.next();
4✔
1333
    }
1334
    while (!currentArgument.isEnd()) {
3✔
1335
      trace("Trying to match argument '{}'", currentArgument);
9✔
1336
      Property<?> currentProperty = property;
2✔
1337
      if (!arguments.isEndOptions()) {
3!
1338
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1339
        if (option != null) {
2!
1340
          currentProperty = option;
×
1341
        }
1342
      }
1343
      if (currentProperty == null) {
2!
1344
        trace("No option or next value found");
×
1345
        ValidationState state = new ValidationState(null);
×
1346
        state.addErrorMessage("No matching property found");
×
1347
        return state;
×
1348
      }
1349
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1350
      if (currentProperty == property) {
3!
1351
        if (!property.isMultiValued()) {
3✔
1352
          if (propertyIterator.hasNext()) {
3✔
1353
            property = propertyIterator.next();
5✔
1354
          } else {
1355
            property = null;
2✔
1356
          }
1357
        }
1358
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1359
          arguments.stopSplitShortOptions();
2✔
1360
        }
1361
      }
1362
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1363
      if (!matches) {
2!
1364
        ValidationState state = new ValidationState(null);
×
1365
        state.addErrorMessage("No matching property found");
×
1366
        return state;
×
1367
      }
1368
      currentArgument = arguments.current();
3✔
1369
    }
1✔
1370
    return ValidationResultValid.get();
2✔
1371
  }
1372

1373
  @Override
1374
  public String findBash() {
1375

1376
    String bash = BASH;
2✔
1377
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1378
      bash = findBashOnWindows();
×
1379
      if (bash == null) {
×
1380
        String variable = IdeVariables.BASH_PATH.getName();
×
1381
        bash = getVariables().get(variable);
×
1382
        if (bash == null) {
×
1383
          trace("Bash not found. Trying to search on system PATH.");
×
1384
          variable = IdeVariables.PATH.getName();
×
1385
          Path plainBash = Path.of(BASH);
×
1386
          Path bashPath = getPath().findBinary(plainBash);
×
1387
          bash = bashPath.toAbsolutePath().toString();
×
1388
          if (bash.contains("AppData\\Local\\Microsoft\\WindowsApps")) {
×
1389
            warning("Only found windows fake bash that is not usable!");
×
1390
            bash = null;
×
1391
          }
1392
        }
1393
        if (bash == null) {
×
1394
          info("Could not find bash in Windows registry, using bash from {} as fallback: {}", variable, bash);
×
1395
        }
1396
      }
1397
    }
1398
    return bash;
2✔
1399
  }
1400

1401
  private String findBashOnWindows() {
1402

1403
    // Check if Git Bash exists in the default location
1404
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1405
    if (Files.exists(defaultPath)) {
×
1406
      return defaultPath.toString();
×
1407
    }
1408

1409
    // If not found in the default location, try the registry query
1410
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1411
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1412
    String regQueryResult;
1413
    for (String bashVariant : bashVariants) {
×
1414
      trace("Trying to find bash variant: {}", bashVariant);
×
1415
      for (String registryKey : registryKeys) {
×
1416
        trace("Trying to find bash from registry key: {}", registryKey);
×
1417
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1418
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1419

1420
        try {
1421
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1422
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1423
            StringBuilder output = new StringBuilder();
×
1424
            String line;
1425

1426
            while ((line = reader.readLine()) != null) {
×
1427
              output.append(line);
×
1428
            }
1429

1430
            int exitCode = process.waitFor();
×
1431
            if (exitCode != 0) {
×
1432
              warning("Query to windows registry for finding bash failed with exit code {}", exitCode);
×
1433
              return null;
×
1434
            }
1435

1436
            regQueryResult = output.toString();
×
1437
            trace("Result from windows registry was: {}", regQueryResult);
×
1438
            int index = regQueryResult.indexOf("REG_SZ");
×
1439
            if (index != -1) {
×
1440
              String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1441
              String bashPath = path + "\\bin\\bash.exe";
×
1442
              debug("Found bash at: {}", bashPath);
×
1443
              return bashPath;
×
1444
            }
1445
          }
×
1446
        } catch (Exception e) {
×
1447
          error(e, "Query to windows registry for finding bash failed!");
×
1448
          return null;
×
1449
        }
×
1450
      }
1451
    }
1452
    // no bash found
1453
    return null;
×
1454
  }
1455

1456
  @Override
1457
  public WindowsPathSyntax getPathSyntax() {
1458

1459
    return this.pathSyntax;
3✔
1460
  }
1461

1462
  /**
1463
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1464
   */
1465
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1466

1467
    this.pathSyntax = pathSyntax;
3✔
1468
  }
1✔
1469

1470
  /**
1471
   * @return the {@link IdeStartContextImpl}.
1472
   */
1473
  public IdeStartContextImpl getStartContext() {
1474

1475
    return startContext;
3✔
1476
  }
1477

1478
  /**
1479
   * @return the {@link WindowsHelper}.
1480
   */
1481
  public final WindowsHelper getWindowsHelper() {
1482

1483
    if (this.windowsHelper == null) {
3✔
1484
      this.windowsHelper = createWindowsHelper();
4✔
1485
    }
1486
    return this.windowsHelper;
3✔
1487
  }
1488

1489
  /**
1490
   * @return the new {@link WindowsHelper} instance.
1491
   */
1492
  protected WindowsHelper createWindowsHelper() {
1493

1494
    return new WindowsHelperImpl(this);
×
1495
  }
1496

1497
  /**
1498
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1499
   */
1500
  public void reload() {
1501

1502
    this.variables = null;
3✔
1503
    this.customToolRepository = null;
3✔
1504
  }
1✔
1505

1506
  @Override
1507
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1508

1509
    assert (Files.isDirectory(installationPath));
6!
1510
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1511
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1512
  }
1✔
1513

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

© 2025 Coveralls, Inc