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

devonfw / IDEasy / 17730250095

15 Sep 2025 10:38AM UTC coverage: 68.686% (+0.01%) from 68.674%
17730250095

push

github

web-flow
#1460: added --no-colors option to disable colored logging and fixed wix logging (#1483)

3401 of 5419 branches covered (62.76%)

Branch coverage included in aggregate %.

8880 of 12461 relevant lines covered (71.26%)

3.13 hits per line

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

63.46
cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java
1
package com.devonfw.tools.ide.context;
2

3
import static com.devonfw.tools.ide.variable.IdeVariables.IDE_MIN_VERSION;
4

5
import java.io.BufferedReader;
6
import java.io.InputStreamReader;
7
import java.net.URL;
8
import java.net.URLConnection;
9
import java.nio.file.Files;
10
import java.nio.file.Path;
11
import java.time.LocalDateTime;
12
import java.util.ArrayList;
13
import java.util.HashMap;
14
import java.util.Iterator;
15
import java.util.List;
16
import java.util.Locale;
17
import java.util.Map;
18
import java.util.Map.Entry;
19
import java.util.Objects;
20

21
import com.devonfw.tools.ide.cli.CliAbortException;
22
import com.devonfw.tools.ide.cli.CliArgument;
23
import com.devonfw.tools.ide.cli.CliArguments;
24
import com.devonfw.tools.ide.cli.CliException;
25
import com.devonfw.tools.ide.commandlet.Commandlet;
26
import com.devonfw.tools.ide.commandlet.CommandletManager;
27
import com.devonfw.tools.ide.commandlet.CommandletManagerImpl;
28
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
29
import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet;
30
import com.devonfw.tools.ide.commandlet.HelpCommandlet;
31
import com.devonfw.tools.ide.common.SystemPath;
32
import com.devonfw.tools.ide.completion.CompletionCandidate;
33
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
34
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault;
35
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
36
import com.devonfw.tools.ide.environment.EnvironmentVariables;
37
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
38
import com.devonfw.tools.ide.environment.IdeSystem;
39
import com.devonfw.tools.ide.environment.IdeSystemImpl;
40
import com.devonfw.tools.ide.git.GitContext;
41
import com.devonfw.tools.ide.git.GitContextImpl;
42
import com.devonfw.tools.ide.git.GitUrl;
43
import com.devonfw.tools.ide.io.FileAccess;
44
import com.devonfw.tools.ide.io.FileAccessImpl;
45
import com.devonfw.tools.ide.log.IdeLogArgFormatter;
46
import com.devonfw.tools.ide.log.IdeLogLevel;
47
import com.devonfw.tools.ide.log.IdeLogger;
48
import com.devonfw.tools.ide.log.IdeSubLogger;
49
import com.devonfw.tools.ide.merge.DirectoryMerger;
50
import com.devonfw.tools.ide.migration.IdeMigrator;
51
import com.devonfw.tools.ide.network.NetworkProxy;
52
import com.devonfw.tools.ide.os.SystemInfo;
53
import com.devonfw.tools.ide.os.SystemInfoImpl;
54
import com.devonfw.tools.ide.os.WindowsHelper;
55
import com.devonfw.tools.ide.os.WindowsHelperImpl;
56
import com.devonfw.tools.ide.os.WindowsPathSyntax;
57
import com.devonfw.tools.ide.process.ProcessContext;
58
import com.devonfw.tools.ide.process.ProcessContextImpl;
59
import com.devonfw.tools.ide.process.ProcessResult;
60
import com.devonfw.tools.ide.property.Property;
61
import com.devonfw.tools.ide.step.Step;
62
import com.devonfw.tools.ide.step.StepImpl;
63
import com.devonfw.tools.ide.tool.repository.CustomToolRepository;
64
import com.devonfw.tools.ide.tool.repository.CustomToolRepositoryImpl;
65
import com.devonfw.tools.ide.tool.repository.DefaultToolRepository;
66
import com.devonfw.tools.ide.tool.repository.MavenRepository;
67
import com.devonfw.tools.ide.tool.repository.ToolRepository;
68
import com.devonfw.tools.ide.url.model.UrlMetadata;
69
import com.devonfw.tools.ide.util.DateTimeUtil;
70
import com.devonfw.tools.ide.util.PrivacyUtil;
71
import com.devonfw.tools.ide.validation.ValidationResult;
72
import com.devonfw.tools.ide.validation.ValidationResultValid;
73
import com.devonfw.tools.ide.validation.ValidationState;
74
import com.devonfw.tools.ide.variable.IdeVariables;
75
import com.devonfw.tools.ide.version.IdeVersion;
76
import com.devonfw.tools.ide.version.VersionIdentifier;
77

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

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

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

88
  private final IdeStartContextImpl startContext;
89

90
  private Path ideHome;
91

92
  private final Path ideRoot;
93

94
  private Path confPath;
95

96
  protected Path settingsPath;
97

98
  private Path settingsCommitIdPath;
99

100
  protected Path pluginsPath;
101

102
  private Path workspacePath;
103

104
  private String workspaceName;
105

106
  private Path cwd;
107

108
  private Path downloadPath;
109

110
  private Path userHome;
111

112
  private Path userHomeIde;
113

114
  private SystemPath path;
115

116
  private WindowsPathSyntax pathSyntax;
117

118
  private final SystemInfo systemInfo;
119

120
  private EnvironmentVariables variables;
121

122
  private final FileAccess fileAccess;
123

124
  protected CommandletManager commandletManager;
125

126
  protected ToolRepository defaultToolRepository;
127

128
  private CustomToolRepository customToolRepository;
129

130
  private final MavenRepository mavenRepository;
131

132
  private DirectoryMerger workspaceMerger;
133

134
  protected UrlMetadata urlMetadata;
135

136
  protected Path defaultExecutionDirectory;
137

138
  private StepImpl currentStep;
139

140
  protected Boolean online;
141

142
  protected IdeSystem system;
143

144
  private NetworkProxy networkProxy;
145

146
  private WindowsHelper windowsHelper;
147

148
  private final Map<String, String> privacyMap;
149

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

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

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

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

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

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

223
  private Path findIdeRoot(Path ideHomePath) {
224

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

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

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

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

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

311
  private String getMessageIdeHomeFound() {
312

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

320
  private String getMessageNotInsideIdeProject() {
321

322
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
323
  }
324

325
  private String getMessageIdeRootNotFound() {
326

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

336
  /**
337
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
338
   */
339
  public boolean isTest() {
340

341
    return false;
×
342
  }
343

344
  protected SystemPath computeSystemPath() {
345

346
    return new SystemPath(this);
×
347
  }
348

349
  private boolean isIdeHome(Path dir) {
350

351
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
352
      return false;
2✔
353
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
354
      return false;
×
355
    }
356
    return true;
2✔
357
  }
358

359
  private EnvironmentVariables createVariables() {
360

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

369
  protected AbstractEnvironmentVariables createSystemVariables() {
370

371
    return EnvironmentVariables.ofSystem(this);
3✔
372
  }
373

374
  @Override
375
  public SystemInfo getSystemInfo() {
376

377
    return this.systemInfo;
3✔
378
  }
379

380
  @Override
381
  public FileAccess getFileAccess() {
382

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

388
  @Override
389
  public CommandletManager getCommandletManager() {
390

391
    return this.commandletManager;
3✔
392
  }
393

394
  @Override
395
  public ToolRepository getDefaultToolRepository() {
396

397
    return this.defaultToolRepository;
3✔
398
  }
399

400
  @Override
401
  public MavenRepository getMavenToolRepository() {
402

403
    return this.mavenRepository;
3✔
404
  }
405

406
  @Override
407
  public CustomToolRepository getCustomToolRepository() {
408

409
    if (this.customToolRepository == null) {
3!
410
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
411
    }
412
    return this.customToolRepository;
3✔
413
  }
414

415
  @Override
416
  public Path getIdeHome() {
417

418
    return this.ideHome;
3✔
419
  }
420

421
  @Override
422
  public String getProjectName() {
423

424
    if (this.ideHome != null) {
3!
425
      return this.ideHome.getFileName().toString();
5✔
426
    }
427
    return "";
×
428
  }
429

430
  @Override
431
  public VersionIdentifier getProjectVersion() {
432

433
    if (this.ideHome != null) {
3!
434
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
435
      if (Files.exists(versionFile)) {
5✔
436
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
437
        return VersionIdentifier.of(version);
3✔
438
      }
439
    }
440
    return IdeMigrator.START_VERSION;
2✔
441
  }
442

443
  @Override
444
  public void setProjectVersion(VersionIdentifier version) {
445

446
    if (this.ideHome == null) {
3!
447
      throw new IllegalStateException("IDE_HOME not available!");
×
448
    }
449
    Objects.requireNonNull(version);
3✔
450
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
451
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
452
  }
1✔
453

454
  @Override
455
  public Path getIdeRoot() {
456

457
    return this.ideRoot;
3✔
458
  }
459

460
  @Override
461
  public Path getIdePath() {
462

463
    Path myIdeRoot = getIdeRoot();
3✔
464
    if (myIdeRoot == null) {
2!
465
      return null;
×
466
    }
467
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
468
  }
469

470
  @Override
471
  public Path getCwd() {
472

473
    return this.cwd;
3✔
474
  }
475

476
  @Override
477
  public Path getTempPath() {
478

479
    Path idePath = getIdePath();
3✔
480
    if (idePath == null) {
2!
481
      return null;
×
482
    }
483
    return idePath.resolve("tmp");
4✔
484
  }
485

486
  @Override
487
  public Path getTempDownloadPath() {
488

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

496
  @Override
497
  public Path getUserHome() {
498

499
    return this.userHome;
3✔
500
  }
501

502
  /**
503
   * This method should only be used for tests to mock user home.
504
   *
505
   * @param userHome the new value of {@link #getUserHome()}.
506
   */
507
  protected void setUserHome(Path userHome) {
508

509
    this.userHome = userHome;
3✔
510
    resetPrivacyMap();
2✔
511
  }
1✔
512

513
  @Override
514
  public Path getUserHomeIde() {
515

516
    return this.userHomeIde;
3✔
517
  }
518

519
  @Override
520
  public Path getSettingsPath() {
521

522
    return this.settingsPath;
3✔
523
  }
524

525
  @Override
526
  public Path getSettingsGitRepository() {
527

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

537
  @Override
538
  public boolean isSettingsRepositorySymlinkOrJunction() {
539

540
    Path settingsPath = getSettingsPath();
3✔
541
    if (settingsPath == null) {
2!
542
      return false;
×
543
    }
544
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
545
  }
546

547
  @Override
548
  public Path getSettingsCommitIdPath() {
549

550
    return this.settingsCommitIdPath;
3✔
551
  }
552

553
  @Override
554
  public Path getConfPath() {
555

556
    return this.confPath;
3✔
557
  }
558

559
  @Override
560
  public Path getSoftwarePath() {
561

562
    if (this.ideHome == null) {
3✔
563
      return null;
2✔
564
    }
565
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
566
  }
567

568
  @Override
569
  public Path getSoftwareExtraPath() {
570

571
    Path softwarePath = getSoftwarePath();
3✔
572
    if (softwarePath == null) {
2!
573
      return null;
×
574
    }
575
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
576
  }
577

578
  @Override
579
  public Path getSoftwareRepositoryPath() {
580

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

588
  @Override
589
  public Path getPluginsPath() {
590

591
    return this.pluginsPath;
3✔
592
  }
593

594
  @Override
595
  public String getWorkspaceName() {
596

597
    return this.workspaceName;
3✔
598
  }
599

600
  @Override
601
  public Path getWorkspacePath() {
602

603
    return this.workspacePath;
3✔
604
  }
605

606
  @Override
607
  public Path getDownloadPath() {
608

609
    return this.downloadPath;
3✔
610
  }
611

612
  @Override
613
  public Path getUrlsPath() {
614

615
    Path idePath = getIdePath();
3✔
616
    if (idePath == null) {
2!
617
      return null;
×
618
    }
619
    return idePath.resolve(FOLDER_URLS);
4✔
620
  }
621

622
  @Override
623
  public Path getToolRepositoryPath() {
624

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

632
  @Override
633
  public SystemPath getPath() {
634

635
    return this.path;
3✔
636
  }
637

638
  @Override
639
  public EnvironmentVariables getVariables() {
640

641
    if (this.variables == null) {
3✔
642
      this.variables = createVariables();
4✔
643
    }
644
    return this.variables;
3✔
645
  }
646

647
  @Override
648
  public UrlMetadata getUrls() {
649

650
    if (this.urlMetadata == null) {
3✔
651
      if (!isTest()) {
3!
652
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
653
      }
654
      this.urlMetadata = new UrlMetadata(this);
6✔
655
    }
656
    return this.urlMetadata;
3✔
657
  }
658

659
  @Override
660
  public boolean isQuietMode() {
661

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

665
  @Override
666
  public boolean isBatchMode() {
667

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

671
  @Override
672
  public boolean isForceMode() {
673

674
    return this.startContext.isForceMode();
4✔
675
  }
676

677
  @Override
678
  public boolean isForcePull() {
679

680
    return this.startContext.isForcePull();
4✔
681
  }
682

683
  @Override
684
  public boolean isForcePlugins() {
685

686
    return this.startContext.isForcePlugins();
4✔
687
  }
688

689
  @Override
690
  public boolean isForceRepositories() {
691

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

695
  @Override
696
  public boolean isOfflineMode() {
697

698
    return this.startContext.isOfflineMode();
4✔
699
  }
700

701
  @Override
702
  public boolean isPrivacyMode() {
703
    return this.startContext.isPrivacyMode();
4✔
704
  }
705

706
  @Override
707
  public boolean isSkipUpdatesMode() {
708

709
    return this.startContext.isSkipUpdatesMode();
4✔
710
  }
711

712
  @Override
713
  public boolean isNoColorsMode() {
714

715
    return this.startContext.isNoColorsMode();
×
716
  }
717

718
  @Override
719
  public boolean isOnline() {
720

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

744
  private void configureNetworkProxy() {
745

746
    if (this.networkProxy == null) {
3✔
747
      this.networkProxy = new NetworkProxy(this);
6✔
748
      this.networkProxy.configure();
3✔
749
    }
750
  }
1✔
751

752
  @Override
753
  public Locale getLocale() {
754

755
    Locale locale = this.startContext.getLocale();
4✔
756
    if (locale == null) {
2✔
757
      locale = Locale.getDefault();
2✔
758
    }
759
    return locale;
2✔
760
  }
761

762
  @Override
763
  public DirectoryMerger getWorkspaceMerger() {
764

765
    if (this.workspaceMerger == null) {
3✔
766
      this.workspaceMerger = new DirectoryMerger(this);
6✔
767
    }
768
    return this.workspaceMerger;
3✔
769
  }
770

771
  /**
772
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
773
   */
774
  @Override
775
  public Path getDefaultExecutionDirectory() {
776

777
    return this.defaultExecutionDirectory;
×
778
  }
779

780
  /**
781
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
782
   */
783
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
784

785
    if (defaultExecutionDirectory != null) {
×
786
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
787
    }
788
  }
×
789

790
  @Override
791
  public GitContext getGitContext() {
792

793
    return new GitContextImpl(this);
×
794
  }
795

796
  @Override
797
  public ProcessContext newProcess() {
798

799
    ProcessContext processContext = createProcessContext();
3✔
800
    if (this.defaultExecutionDirectory != null) {
3!
801
      processContext.directory(this.defaultExecutionDirectory);
×
802
    }
803
    return processContext;
2✔
804
  }
805

806
  @Override
807
  public IdeSystem getSystem() {
808

809
    if (this.system == null) {
×
810
      this.system = new IdeSystemImpl(this);
×
811
    }
812
    return this.system;
×
813
  }
814

815
  /**
816
   * @return a new instance of {@link ProcessContext}.
817
   * @see #newProcess()
818
   */
819
  protected ProcessContext createProcessContext() {
820

821
    return new ProcessContextImpl(this);
5✔
822
  }
823

824
  @Override
825
  public IdeSubLogger level(IdeLogLevel level) {
826

827
    return this.startContext.level(level);
5✔
828
  }
829

830
  @Override
831
  public void logIdeHomeAndRootStatus() {
832
    if (this.ideRoot != null) {
3!
833
      success("IDE_ROOT is set to {}", this.ideRoot);
×
834
    }
835
    if (this.ideHome == null) {
3✔
836
      warning(getMessageNotInsideIdeProject());
5✔
837
    } else {
838
      success("IDE_HOME is set to {}", this.ideHome);
10✔
839
    }
840
  }
1✔
841

842
  @Override
843
  public String formatArgument(Object argument) {
844

845
    if (argument == null) {
2✔
846
      return null;
2✔
847
    }
848
    String result = argument.toString();
3✔
849
    if (isPrivacyMode()) {
3✔
850
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
851
        initializePrivacyMap(this.userHome, "~");
×
852
        String projectName = getProjectName();
×
853
        if (!projectName.isEmpty()) {
×
854
          this.privacyMap.put(projectName, "project");
×
855
        }
856
      }
857
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
858
        result = result.replace(entry.getKey(), entry.getValue());
×
859
      }
×
860
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
861
    }
862
    return result;
2✔
863
  }
864

865
  /**
866
   * @param path the sensitive {@link Path} to
867
   * @param replacement the replacement to mask the {@link Path} in log output.
868
   */
869
  protected void initializePrivacyMap(Path path, String replacement) {
870

871
    if (path == null) {
×
872
      return;
×
873
    }
874
    if (this.systemInfo.isWindows()) {
×
875
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
876
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
877
    } else {
878
      this.privacyMap.put(path.toString(), replacement);
×
879
    }
880
  }
×
881

882
  /**
883
   * Resets the privacy map in case fundamental values have changed.
884
   */
885
  private void resetPrivacyMap() {
886

887
    this.privacyMap.clear();
3✔
888
  }
1✔
889

890

891
  @Override
892
  public String askForInput(String message, String defaultValue) {
893

894
    while (true) {
895
      if (!message.isBlank()) {
3!
896
        interaction(message);
3✔
897
      }
898
      if (isBatchMode()) {
3!
899
        if (isForceMode()) {
×
900
          return defaultValue;
×
901
        } else {
902
          throw new CliAbortException();
×
903
        }
904
      }
905
      String input = readLine().trim();
4✔
906
      if (!input.isEmpty()) {
3!
907
        return input;
2✔
908
      } else {
909
        if (defaultValue != null) {
×
910
          return defaultValue;
×
911
        }
912
      }
913
    }
×
914
  }
915

916
  @SuppressWarnings("unchecked")
917
  @Override
918
  public <O> O question(O[] options, String question, Object... args) {
919

920
    assert (options.length >= 2);
5!
921
    interaction(question, args);
4✔
922
    return displayOptionsAndGetAnswer(options);
4✔
923
  }
924

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

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

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

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

971
  @Override
972
  public Step getCurrentStep() {
973

974
    return this.currentStep;
×
975
  }
976

977
  @Override
978
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
979

980
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
981
    return this.currentStep;
3✔
982
  }
983

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

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

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

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

1050
  @Override
1051
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1052

1053
    this.startContext.deactivateLogging(threshold);
4✔
1054
    lambda.run();
2✔
1055
    this.startContext.activateLogging();
3✔
1056
  }
1✔
1057

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

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

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

1122
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1123

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

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

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

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

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

1235
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1236

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

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

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

1347
  @Override
1348
  public String findBash() {
1349

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

1375
  private String findBashOnWindows() {
1376

1377
    // Check if Git Bash exists in the default location
1378
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1379
    if (Files.exists(defaultPath)) {
×
1380
      return defaultPath.toString();
×
1381
    }
1382

1383
    // If not found in the default location, try the registry query
1384
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1385
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1386
    String regQueryResult;
1387
    for (String bashVariant : bashVariants) {
×
1388
      trace("Trying to find bash variant: {}", bashVariant);
×
1389
      for (String registryKey : registryKeys) {
×
1390
        trace("Trying to find bash from registry key: {}", registryKey);
×
1391
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1392
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1393

1394
        try {
1395
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1396
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1397
            StringBuilder output = new StringBuilder();
×
1398
            String line;
1399

1400
            while ((line = reader.readLine()) != null) {
×
1401
              output.append(line);
×
1402
            }
1403

1404
            int exitCode = process.waitFor();
×
1405
            if (exitCode != 0) {
×
1406
              warning("Query to windows registry for finding bash failed with exit code {}", exitCode);
×
1407
              return null;
×
1408
            }
1409

1410
            regQueryResult = output.toString();
×
1411
            trace("Result from windows registry was: {}", regQueryResult);
×
1412
            int index = regQueryResult.indexOf("REG_SZ");
×
1413
            if (index != -1) {
×
1414
              String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1415
              String bashPath = path + "\\bin\\bash.exe";
×
1416
              debug("Found bash at: {}", bashPath);
×
1417
              return bashPath;
×
1418
            }
1419
          }
×
1420
        } catch (Exception e) {
×
1421
          error(e, "Query to windows registry for finding bash failed!");
×
1422
          return null;
×
1423
        }
×
1424
      }
1425
    }
1426
    // no bash found
1427
    return null;
×
1428
  }
1429

1430
  @Override
1431
  public WindowsPathSyntax getPathSyntax() {
1432

1433
    return this.pathSyntax;
3✔
1434
  }
1435

1436
  /**
1437
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1438
   */
1439
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1440

1441
    this.pathSyntax = pathSyntax;
3✔
1442
  }
1✔
1443

1444
  /**
1445
   * @return the {@link IdeStartContextImpl}.
1446
   */
1447
  public IdeStartContextImpl getStartContext() {
1448

1449
    return startContext;
3✔
1450
  }
1451

1452
  /**
1453
   * @return the {@link WindowsHelper}.
1454
   */
1455
  public final WindowsHelper getWindowsHelper() {
1456

1457
    if (this.windowsHelper == null) {
3✔
1458
      this.windowsHelper = createWindowsHelper();
4✔
1459
    }
1460
    return this.windowsHelper;
3✔
1461
  }
1462

1463
  /**
1464
   * @return the new {@link WindowsHelper} instance.
1465
   */
1466
  protected WindowsHelper createWindowsHelper() {
1467

1468
    return new WindowsHelperImpl(this);
×
1469
  }
1470

1471
  /**
1472
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1473
   */
1474
  public void reload() {
1475

1476
    this.variables = null;
3✔
1477
    this.customToolRepository = null;
3✔
1478
  }
1✔
1479

1480
  @Override
1481
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1482

1483
    assert (Files.isDirectory(installationPath));
6!
1484
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1485
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1486
  }
1✔
1487

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