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

devonfw / IDEasy / 15438009664

04 Jun 2025 08:54AM UTC coverage: 67.693% (-0.04%) from 67.73%
15438009664

Pull #1350

github

web-flow
Merge e91ebdccc into ad516c95b
Pull Request #1350: #1340: proper fix if IDE_ROOT is not sane

3165 of 5080 branches covered (62.3%)

Branch coverage included in aggregate %.

8116 of 11585 relevant lines covered (70.06%)

3.07 hits per line

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

61.23
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.Objects;
19

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

75
/**
76
 * Abstract base implementation of {@link IdeContext}.
77
 */
78
public abstract class AbstractIdeContext implements IdeContext {
79

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

82
  private static final String LICENSE_URL = "https://github.com/devonfw/IDEasy/blob/main/documentation/LICENSE.adoc";
83

84
  private final IdeStartContextImpl startContext;
85

86
  private Path ideHome;
87

88
  private final Path ideRoot;
89

90
  private Path confPath;
91

92
  protected Path settingsPath;
93

94
  private Path settingsCommitIdPath;
95

96
  protected Path pluginsPath;
97

98
  private Path workspacePath;
99

100
  private String workspaceName;
101

102
  private Path cwd;
103

104
  private Path downloadPath;
105

106
  protected Path userHome;
107

108
  private Path userHomeIde;
109

110
  private SystemPath path;
111

112
  private WindowsPathSyntax pathSyntax;
113

114
  private final SystemInfo systemInfo;
115

116
  private EnvironmentVariables variables;
117

118
  private final FileAccess fileAccess;
119

120
  protected CommandletManager commandletManager;
121

122
  protected ToolRepository defaultToolRepository;
123

124
  private CustomToolRepository customToolRepository;
125

126
  private MavenRepository mavenRepository;
127

128
  private DirectoryMerger workspaceMerger;
129

130
  protected UrlMetadata urlMetadata;
131

132
  protected Path defaultExecutionDirectory;
133

134
  private StepImpl currentStep;
135

136
  protected Boolean online;
137

138
  protected IdeSystem system;
139

140
  private NetworkProxy networkProxy;
141

142
  private WindowsHelper windowsHelper;
143

144
  /**
145
   * The constructor.
146
   *
147
   * @param startContext the {@link IdeLogger}.
148
   * @param workingDirectory the optional {@link Path} to current working directory.
149
   */
150
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
151

152
    super();
2✔
153
    this.startContext = startContext;
3✔
154
    this.systemInfo = SystemInfoImpl.INSTANCE;
3✔
155
    this.commandletManager = new CommandletManagerImpl(this);
6✔
156
    this.fileAccess = new FileAccessImpl(this);
6✔
157
    String workspace = WORKSPACE_MAIN;
2✔
158
    if (workingDirectory == null) {
2!
159
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
160
    }
161
    workingDirectory = workingDirectory.toAbsolutePath();
3✔
162
    this.cwd = workingDirectory;
3✔
163
    // detect IDE_HOME and WORKSPACE
164
    Path currentDir = workingDirectory;
2✔
165
    String name1 = "";
2✔
166
    String name2 = "";
2✔
167
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
168
    while (currentDir != null) {
2✔
169
      trace("Looking for IDE_HOME in {}", currentDir);
9✔
170
      if (isIdeHome(currentDir)) {
4✔
171
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
172
          workspace = name2;
3✔
173
        }
174
        break;
175
      }
176
      name2 = name1;
2✔
177
      int nameCount = currentDir.getNameCount();
3✔
178
      if (nameCount >= 1) {
3✔
179
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
180
      }
181
      currentDir = currentDir.getParent();
3✔
182
      if ((ideRootPath != null) && (ideRootPath.equals(currentDir))) {
2!
183
        // prevent that during tests we traverse to the real IDE project of IDEasy developer
184
        currentDir = null;
×
185
      }
186
    }
1✔
187

188
    // detection completed, initializing variables
189
    this.ideRoot = findIdeRoot(currentDir);
5✔
190

191
    setCwd(workingDirectory, workspace, currentDir);
5✔
192

193
    if (this.ideRoot != null) {
3✔
194
      Path tempDownloadPath = getTempDownloadPath();
3✔
195
      if (Files.isDirectory(tempDownloadPath)) {
6✔
196
        // TODO delete all files older than 1 day here...
197
      } else {
198
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
199
      }
200
    }
201

202
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
203
    this.mavenRepository = new MavenRepository(this);
6✔
204
  }
1✔
205

206
  private Path findIdeRoot(Path ideHomePath) {
207

208
    Path ideRootPath = null;
2✔
209
    if (ideHomePath != null) {
2✔
210
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
211
      ideRootPath = ideHomePath.getParent();
3✔
212
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
8!
213
        warning("Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.", ideRootPathFromEnv,
16✔
214
            ideRootPath, ideHomePath.getFileName());
2✔
215
      }
216

217
    } else if (!isTest()) {
4!
218
      ideRootPath = getIdeRootPathFromEnv(true);
×
219
    }
220
    return ideRootPath;
2✔
221
  }
222

223
  /**
224
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
225
   */
226
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
227

228
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
229
    if (root != null) {
×
230
      Path rootPath = Path.of(root);
×
231
      if (Files.isDirectory(rootPath)) {
×
232
        Path absoluteRootPath = getFileAccess().toCanonicalPath(rootPath);
×
233
        if (withSanityCheck) {
×
234
          int nameCount = rootPath.getNameCount();
×
235
          int absoluteNameCount = absoluteRootPath.getNameCount();
×
236
          int delta = absoluteNameCount - nameCount;
×
237
          if (delta >= 0) {
×
238
            for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) {
×
239
              String rootName = rootPath.getName(nameIndex).toString();
×
240
              String absoluteRootName = absoluteRootPath.getName(nameIndex + delta).toString();
×
241
              if (!rootName.equals(absoluteRootName)) {
×
242
                warning("IDE_ROOT is set to {} but was expanded to absolute path {} and does not match for segment {} and {} - fix your IDEasy installation!",
×
243
                    rootPath, absoluteRootPath, rootName, absoluteRootName);
244
                break;
×
245
              }
246
            }
247
          } else {
248
            warning("IDE_ROOT is set to {} but was expanded to a shorter absolute path {}", rootPath, absoluteRootPath);
×
249
          }
250
        }
251
        return absoluteRootPath;
×
252
      } else if (withSanityCheck) {
×
253
        warning("IDE_ROOT is set to {} that is not an existing directory - fix your IDEasy installation!", rootPath);
×
254
      }
255
    }
256
    return null;
×
257
  }
258

259
  @Override
260
  public void setCwd(Path userDir, String workspace, Path ideHome) {
261

262
    this.cwd = userDir;
3✔
263
    this.workspaceName = workspace;
3✔
264
    this.ideHome = ideHome;
3✔
265
    if (ideHome == null) {
2✔
266
      this.workspacePath = null;
3✔
267
      this.confPath = null;
3✔
268
      this.settingsPath = null;
3✔
269
      this.pluginsPath = null;
4✔
270
    } else {
271
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
272
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
273
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
274
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
275
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
276
    }
277
    if (isTest()) {
3!
278
      // only for testing...
279
      if (this.ideHome == null) {
3✔
280
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
281
      } else {
282
        this.userHome = this.ideHome.resolve("home");
7✔
283
      }
284
    } else {
285
      this.userHome = Path.of(getSystem().getProperty("user.home"));
×
286
    }
287
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
288
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
289

290
    this.path = computeSystemPath();
4✔
291
  }
1✔
292

293
  private String getMessageIdeHomeFound() {
294

295
    return "IDE environment variables have been set for " + this.ideHome + " in workspace " + this.workspaceName;
7✔
296
  }
297

298
  private String getMessageNotInsideIdeProject() {
299

300
    return "You are not inside an IDE project: " + this.cwd;
5✔
301
  }
302

303
  private String getMessageIdeRootNotFound() {
304

305
    String root = getSystem().getEnv("IDE_ROOT");
5✔
306
    if (root == null) {
2!
307
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
308
    } else {
309
      return "The environment variable IDE_ROOT is pointing to an invalid path " + root + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
×
310
    }
311
  }
312

313
  /**
314
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
315
   */
316
  public boolean isTest() {
317

318
    return false;
×
319
  }
320

321
  protected SystemPath computeSystemPath() {
322

323
    return new SystemPath(this);
×
324
  }
325

326
  private boolean isIdeHome(Path dir) {
327

328
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
329
      return false;
2✔
330
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
331
      return false;
×
332
    }
333
    return true;
2✔
334
  }
335

336
  private EnvironmentVariables createVariables() {
337

338
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
339
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
340
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
341
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
342
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
343
    return conf.resolved();
3✔
344
  }
345

346
  protected AbstractEnvironmentVariables createSystemVariables() {
347

348
    return EnvironmentVariables.ofSystem(this);
3✔
349
  }
350

351
  @Override
352
  public SystemInfo getSystemInfo() {
353

354
    return this.systemInfo;
3✔
355
  }
356

357
  @Override
358
  public FileAccess getFileAccess() {
359

360
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
361
    configureNetworkProxy();
2✔
362
    return this.fileAccess;
3✔
363
  }
364

365
  @Override
366
  public CommandletManager getCommandletManager() {
367

368
    return this.commandletManager;
3✔
369
  }
370

371
  @Override
372
  public ToolRepository getDefaultToolRepository() {
373

374
    return this.defaultToolRepository;
3✔
375
  }
376

377
  @Override
378
  public MavenRepository getMavenToolRepository() {
379

380
    return this.mavenRepository;
3✔
381
  }
382

383
  @Override
384
  public CustomToolRepository getCustomToolRepository() {
385

386
    if (this.customToolRepository == null) {
3!
387
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
388
    }
389
    return this.customToolRepository;
3✔
390
  }
391

392
  @Override
393
  public Path getIdeHome() {
394

395
    return this.ideHome;
3✔
396
  }
397

398
  @Override
399
  public String getProjectName() {
400

401
    if (this.ideHome != null) {
3!
402
      return this.ideHome.getFileName().toString();
5✔
403
    }
404
    return "";
×
405
  }
406

407
  @Override
408
  public VersionIdentifier getProjectVersion() {
409

410
    if (this.ideHome != null) {
3!
411
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
412
      if (Files.exists(versionFile)) {
5✔
413
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
414
        return VersionIdentifier.of(version);
3✔
415
      }
416
    }
417
    return IdeMigrator.START_VERSION;
2✔
418
  }
419

420
  @Override
421
  public void setProjectVersion(VersionIdentifier version) {
422

423
    if (this.ideHome == null) {
3!
424
      throw new IllegalStateException("IDE_HOME not available!");
×
425
    }
426
    Objects.requireNonNull(version);
3✔
427
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
428
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
429
  }
1✔
430

431
  @Override
432
  public Path getIdeRoot() {
433

434
    return this.ideRoot;
3✔
435
  }
436

437
  @Override
438
  public Path getIdePath() {
439

440
    Path myIdeRoot = getIdeRoot();
3✔
441
    if (myIdeRoot == null) {
2!
442
      return null;
×
443
    }
444
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
445
  }
446

447
  @Override
448
  public Path getCwd() {
449

450
    return this.cwd;
3✔
451
  }
452

453
  @Override
454
  public Path getTempPath() {
455

456
    Path idePath = getIdePath();
3✔
457
    if (idePath == null) {
2!
458
      return null;
×
459
    }
460
    return idePath.resolve("tmp");
4✔
461
  }
462

463
  @Override
464
  public Path getTempDownloadPath() {
465

466
    Path tmp = getTempPath();
3✔
467
    if (tmp == null) {
2!
468
      return null;
×
469
    }
470
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
471
  }
472

473
  @Override
474
  public Path getUserHome() {
475

476
    return this.userHome;
3✔
477
  }
478

479
  @Override
480
  public Path getUserHomeIde() {
481

482
    return this.userHomeIde;
3✔
483
  }
484

485
  @Override
486
  public Path getSettingsPath() {
487

488
    return this.settingsPath;
3✔
489
  }
490

491
  @Override
492
  public Path getSettingsGitRepository() {
493

494
    Path settingsPath = getSettingsPath();
3✔
495
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
496
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
497
      error("Settings repository exists but is not a git repository.");
3✔
498
      return null;
2✔
499
    }
500
    return settingsPath;
2✔
501
  }
502

503
  @Override
504
  public boolean isSettingsRepositorySymlinkOrJunction() {
505

506
    Path settingsPath = getSettingsPath();
3✔
507
    if (settingsPath == null) {
2!
508
      return false;
×
509
    }
510
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
511
  }
512

513
  @Override
514
  public Path getSettingsCommitIdPath() {
515

516
    return this.settingsCommitIdPath;
3✔
517
  }
518

519
  @Override
520
  public Path getConfPath() {
521

522
    return this.confPath;
3✔
523
  }
524

525
  @Override
526
  public Path getSoftwarePath() {
527

528
    if (this.ideHome == null) {
3✔
529
      return null;
2✔
530
    }
531
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
532
  }
533

534
  @Override
535
  public Path getSoftwareExtraPath() {
536

537
    Path softwarePath = getSoftwarePath();
3✔
538
    if (softwarePath == null) {
2!
539
      return null;
×
540
    }
541
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
542
  }
543

544
  @Override
545
  public Path getSoftwareRepositoryPath() {
546

547
    Path idePath = getIdePath();
3✔
548
    if (idePath == null) {
2!
549
      return null;
×
550
    }
551
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
552
  }
553

554
  @Override
555
  public Path getPluginsPath() {
556

557
    return this.pluginsPath;
3✔
558
  }
559

560
  @Override
561
  public String getWorkspaceName() {
562

563
    return this.workspaceName;
3✔
564
  }
565

566
  @Override
567
  public Path getWorkspacePath() {
568

569
    return this.workspacePath;
3✔
570
  }
571

572
  @Override
573
  public Path getDownloadPath() {
574

575
    return this.downloadPath;
3✔
576
  }
577

578
  @Override
579
  public Path getUrlsPath() {
580

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

588
  @Override
589
  public Path getToolRepositoryPath() {
590

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

598
  @Override
599
  public SystemPath getPath() {
600

601
    return this.path;
3✔
602
  }
603

604
  @Override
605
  public EnvironmentVariables getVariables() {
606

607
    if (this.variables == null) {
3✔
608
      this.variables = createVariables();
4✔
609
    }
610
    return this.variables;
3✔
611
  }
612

613
  @Override
614
  public UrlMetadata getUrls() {
615

616
    if (this.urlMetadata == null) {
3✔
617
      if (!isTest()) {
3!
618
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
619
      }
620
      this.urlMetadata = new UrlMetadata(this);
6✔
621
    }
622
    return this.urlMetadata;
3✔
623
  }
624

625
  @Override
626
  public boolean isQuietMode() {
627

628
    return this.startContext.isQuietMode();
4✔
629
  }
630

631
  @Override
632
  public boolean isBatchMode() {
633

634
    return this.startContext.isBatchMode();
4✔
635
  }
636

637
  @Override
638
  public boolean isForceMode() {
639

640
    return this.startContext.isForceMode();
4✔
641
  }
642

643
  @Override
644
  public boolean isForcePull() {
645

646
    return this.startContext.isForcePull();
×
647
  }
648

649
  @Override
650
  public boolean isForcePlugins() {
651

652
    return this.startContext.isForcePlugins();
×
653
  }
654

655
  @Override
656
  public boolean isForceRepositories() {
657

658
    return this.startContext.isForceRepositories();
×
659
  }
660

661
  @Override
662
  public boolean isOfflineMode() {
663

664
    return this.startContext.isOfflineMode();
4✔
665
  }
666

667
  @Override
668
  public boolean isSkipUpdatesMode() {
669

670
    return this.startContext.isSkipUpdatesMode();
4✔
671
  }
672

673
  @Override
674
  public boolean isOnline() {
675

676
    if (this.online == null) {
3✔
677
      configureNetworkProxy();
2✔
678
      // we currently assume we have only a CLI process that runs shortly
679
      // therefore we run this check only once to save resources when this method is called many times
680
      try {
681
        int timeout = 1000;
2✔
682
        //open a connection to github.com and try to retrieve data
683
        //getContent fails if there is no connection
684
        URLConnection connection = new URL("https://www.github.com").openConnection();
6✔
685
        connection.setConnectTimeout(timeout);
3✔
686
        connection.getContent();
3✔
687
        this.online = Boolean.TRUE;
3✔
688
      } catch (Exception ignored) {
×
689
        this.online = Boolean.FALSE;
×
690
      }
1✔
691
    }
692
    return this.online.booleanValue();
4✔
693
  }
694

695
  private void configureNetworkProxy() {
696

697
    if (this.networkProxy == null) {
3✔
698
      this.networkProxy = new NetworkProxy(this);
6✔
699
      this.networkProxy.configure();
3✔
700
    }
701
  }
1✔
702

703
  @Override
704
  public Locale getLocale() {
705

706
    Locale locale = this.startContext.getLocale();
4✔
707
    if (locale == null) {
2✔
708
      locale = Locale.getDefault();
2✔
709
    }
710
    return locale;
2✔
711
  }
712

713
  @Override
714
  public DirectoryMerger getWorkspaceMerger() {
715

716
    if (this.workspaceMerger == null) {
3✔
717
      this.workspaceMerger = new DirectoryMerger(this);
6✔
718
    }
719
    return this.workspaceMerger;
3✔
720
  }
721

722
  /**
723
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
724
   */
725
  @Override
726
  public Path getDefaultExecutionDirectory() {
727

728
    return this.defaultExecutionDirectory;
×
729
  }
730

731
  /**
732
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
733
   */
734
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
735

736
    if (defaultExecutionDirectory != null) {
×
737
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
738
    }
739
  }
×
740

741
  @Override
742
  public GitContext getGitContext() {
743

744
    return new GitContextImpl(this);
×
745
  }
746

747
  @Override
748
  public ProcessContext newProcess() {
749

750
    ProcessContext processContext = createProcessContext();
3✔
751
    if (this.defaultExecutionDirectory != null) {
3!
752
      processContext.directory(this.defaultExecutionDirectory);
×
753
    }
754
    return processContext;
2✔
755
  }
756

757
  @Override
758
  public IdeSystem getSystem() {
759

760
    if (this.system == null) {
×
761
      this.system = new IdeSystemImpl(this);
×
762
    }
763
    return this.system;
×
764
  }
765

766
  /**
767
   * @return a new instance of {@link ProcessContext}.
768
   * @see #newProcess()
769
   */
770
  protected ProcessContext createProcessContext() {
771

772
    return new ProcessContextImpl(this);
5✔
773
  }
774

775
  @Override
776
  public IdeSubLogger level(IdeLogLevel level) {
777

778
    return this.startContext.level(level);
5✔
779
  }
780

781
  @Override
782
  public void logIdeHomeAndRootStatus() {
783

784
    if (this.ideRoot != null) {
3!
785
      success("IDE_ROOT is set to {}", this.ideRoot);
×
786
    }
787
    if (this.ideHome == null) {
3!
788
      warning(getMessageNotInsideIdeProject());
5✔
789
    } else {
790
      success("IDE_HOME is set to {}", this.ideHome);
×
791
    }
792
  }
1✔
793

794
  @Override
795
  public String askForInput(String message, String defaultValue) {
796

797
    if (!message.isBlank()) {
×
798
      info(message);
×
799
    }
800
    if (isBatchMode()) {
×
801
      if (isForceMode() || isForcePull()) {
×
802
        return defaultValue;
×
803
      } else {
804
        throw new CliAbortException();
×
805
      }
806
    }
807
    String input = readLine().trim();
×
808
    return input.isEmpty() ? defaultValue : input;
×
809
  }
810

811
  @Override
812
  public String askForInput(String message) {
813

814
    String input;
815
    do {
816
      info(message);
3✔
817
      input = readLine().trim();
4✔
818
    } while (input.isEmpty());
3!
819

820
    return input;
2✔
821
  }
822

823
  @SuppressWarnings("unchecked")
824
  @Override
825
  public <O> O question(String question, O... options) {
826

827
    assert (options.length >= 2);
×
828
    interaction(question);
×
829
    Map<String, O> mapping = new HashMap<>(options.length);
×
830
    int i = 0;
×
831
    for (O option : options) {
×
832
      i++;
×
833
      String key = "" + option;
×
834
      addMapping(mapping, key, option);
×
835
      String numericKey = Integer.toString(i);
×
836
      if (numericKey.equals(key)) {
×
837
        trace("Options should not be numeric: " + key);
×
838
      } else {
839
        addMapping(mapping, numericKey, option);
×
840
      }
841
      interaction("Option " + numericKey + ": " + key);
×
842
    }
843
    O option = null;
×
844
    if (isBatchMode()) {
×
845
      if (isForceMode() || isForcePull()) {
×
846
        option = options[0];
×
847
        interaction("" + option);
×
848
      }
849
    } else {
850
      while (option == null) {
×
851
        String answer = readLine();
×
852
        option = mapping.get(answer);
×
853
        if (option == null) {
×
854
          warning("Invalid answer: '" + answer + "' - please try again.");
×
855
        }
856
      }
×
857
    }
858
    return option;
×
859
  }
860

861
  /**
862
   * @return the input from the end-user (e.g. read from the console).
863
   */
864
  protected abstract String readLine();
865

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

868
    O duplicate = mapping.put(key, option);
×
869
    if (duplicate != null) {
×
870
      throw new IllegalArgumentException("Duplicated option " + key);
×
871
    }
872
  }
×
873

874
  @Override
875
  public Step getCurrentStep() {
876

877
    return this.currentStep;
×
878
  }
879

880
  @Override
881
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
882

883
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
884
    return this.currentStep;
3✔
885
  }
886

887
  /**
888
   * Internal method to end the running {@link Step}.
889
   *
890
   * @param step the current {@link Step} to end.
891
   */
892
  public void endStep(StepImpl step) {
893

894
    if (step == this.currentStep) {
4!
895
      this.currentStep = this.currentStep.getParent();
6✔
896
    } else {
897
      String currentStepName = "null";
×
898
      if (this.currentStep != null) {
×
899
        currentStepName = this.currentStep.getName();
×
900
      }
901
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
902
    }
903
  }
1✔
904

905
  /**
906
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
907
   *
908
   * @param arguments the {@link CliArgument}.
909
   * @return the return code of the execution.
910
   */
911
  public int run(CliArguments arguments) {
912

913
    CliArgument current = arguments.current();
3✔
914
    assert (this.currentStep == null);
4!
915
    boolean supressStepSuccess = false;
2✔
916
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
917
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
918
    Commandlet cmd = null;
2✔
919
    ValidationResult result = null;
2✔
920
    try {
921
      while (commandletIterator.hasNext()) {
3✔
922
        cmd = commandletIterator.next();
4✔
923
        result = applyAndRun(arguments.copy(), cmd);
6✔
924
        if (result.isValid()) {
3!
925
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
926
          step.success();
2✔
927
          return ProcessResult.SUCCESS;
4✔
928
        }
929
      }
930
      this.startContext.activateLogging();
3✔
931
      verifyIdeMinVersion(false);
3✔
932
      if (result != null) {
2!
933
        error(result.getErrorMessage());
×
934
      }
935
      step.error("Invalid arguments: {}", current.getArgs());
10✔
936
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
937
      if (cmd != null) {
2!
938
        help.commandlet.setValue(cmd);
×
939
      }
940
      help.run();
2✔
941
      return 1;
4✔
942
    } catch (Throwable t) {
1✔
943
      this.startContext.activateLogging();
3✔
944
      step.error(t, true);
4✔
945
      throw t;
2✔
946
    } finally {
947
      step.close();
2✔
948
      assert (this.currentStep == null);
4!
949
      step.logSummary(supressStepSuccess);
3✔
950
    }
951
  }
952

953
  @Override
954
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
955

956
    this.startContext.deactivateLogging(threshold);
4✔
957
    lambda.run();
2✔
958
    this.startContext.activateLogging();
3✔
959
  }
1✔
960

961
  /**
962
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
963
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
964
   *     {@link Commandlet} did not match and we have to try a different candidate).
965
   */
966
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
967

968
    IdeLogLevel previousLogLevel = null;
2✔
969
    cmd.reset();
2✔
970
    ValidationResult result = apply(arguments, cmd);
5✔
971
    if (result.isValid()) {
3!
972
      result = cmd.validate();
3✔
973
    }
974
    if (result.isValid()) {
3!
975
      debug("Running commandlet {}", cmd);
9✔
976
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
977
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
978
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
979
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
980
      }
981
      try {
982
        if (cmd.isProcessableOutput()) {
3!
983
          if (!debug().isEnabled()) {
×
984
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
985
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
986
          }
987
          this.startContext.activateLogging();
×
988
        } else {
989
          this.startContext.activateLogging();
3✔
990
          if (cmd.isIdeHomeRequired()) {
3!
991
            debug(getMessageIdeHomeFound());
4✔
992
          }
993
          Path settingsRepository = getSettingsGitRepository();
3✔
994
          if (settingsRepository != null) {
2!
995
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
996
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
997
                    settingsRepository, getSettingsCommitIdPath()))) {
×
998
              if (isSettingsRepositorySymlinkOrJunction()) {
×
999
                interaction(
×
1000
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
1001

1002
              } else {
1003
                interaction(
×
1004
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
1005
              }
1006
            }
1007
          }
1008
        }
1009
        boolean success = ensureLicenseAgreement(cmd);
4✔
1010
        if (!success) {
2!
1011
          return ValidationResultValid.get();
×
1012
        }
1013
        cmd.run();
2✔
1014
      } finally {
1015
        if (previousLogLevel != null) {
2!
1016
          this.startContext.setLogLevel(previousLogLevel);
×
1017
        }
1018
      }
1✔
1019
    } else {
1020
      trace("Commandlet did not match");
×
1021
    }
1022
    return result;
2✔
1023
  }
1024

1025
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1026

1027
    if (isTest()) {
3!
1028
      return true; // ignore for tests
2✔
1029
    }
1030
    getFileAccess().mkdirs(this.userHomeIde);
×
1031
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1032
    if (Files.isRegularFile(licenseAgreement)) {
×
1033
      return true; // success, license already accepted
×
1034
    }
1035
    if (cmd instanceof EnvironmentCommandlet) {
×
1036
      // 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
1037
      // 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
1038
      // printing anything anymore in such case.
1039
      return false;
×
1040
    }
1041
    boolean logLevelInfoDisabled = !this.startContext.info().isEnabled();
×
1042
    if (logLevelInfoDisabled) {
×
1043
      this.startContext.setLogLevel(IdeLogLevel.INFO, true);
×
1044
    }
1045
    boolean logLevelInteractionDisabled = !this.startContext.interaction().isEnabled();
×
1046
    if (logLevelInteractionDisabled) {
×
1047
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, true);
×
1048
    }
1049
    StringBuilder sb = new StringBuilder(1180);
×
1050
    sb.append(LOGO).append("""
×
1051
        Welcome to IDEasy!
1052
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1053
        It supports automatic download and installation of arbitrary 3rd party tools.
1054
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1055
        But if explicitly configured, also commercial software that requires an additional license may be used.
1056
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1057
        You are solely responsible for all risks implied by using this software.
1058
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1059
        You will be able to find it online under the following URL:
1060
        """).append(LICENSE_URL);
×
1061
    if (this.ideRoot != null) {
×
1062
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1063
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1064
    }
1065
    info(sb.toString());
×
1066
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1067

1068
    sb.setLength(0);
×
1069
    LocalDateTime now = LocalDateTime.now();
×
1070
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1071
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1072
    try {
1073
      Files.writeString(licenseAgreement, sb);
×
1074
    } catch (Exception e) {
×
1075
      throw new RuntimeException("Failed to save license agreement!", e);
×
1076
    }
×
1077
    if (logLevelInfoDisabled) {
×
1078
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
1079
    }
1080
    if (logLevelInteractionDisabled) {
×
1081
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
1082
    }
1083
    return true;
×
1084
  }
1085

1086
  @Override
1087
  public void verifyIdeMinVersion(boolean throwException) {
1088
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1089
    if (minVersion == null) {
2✔
1090
      return;
1✔
1091
    }
1092
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1093
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1094
          + "However, this is too old as your project requires at latest version %s\n"
1095
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1096
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1097
      if (throwException) {
2✔
1098
        throw new CliException(message);
5✔
1099
      } else {
1100
        warning(message);
3✔
1101
      }
1102
    }
1103
  }
1✔
1104

1105
  /**
1106
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1107
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1108
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1109
   */
1110
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1111

1112
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1113
    if (arguments.current().isStart()) {
4✔
1114
      arguments.next();
3✔
1115
    }
1116
    if (includeContextOptions) {
2✔
1117
      ContextCommandlet cc = new ContextCommandlet();
4✔
1118
      for (Property<?> property : cc.getProperties()) {
11✔
1119
        assert (property.isOption());
4!
1120
        property.apply(arguments, this, cc, collector);
7✔
1121
      }
1✔
1122
    }
1123
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1124
    CliArgument current = arguments.current();
3✔
1125
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1126
      collector.add(current.get(), null, null, null);
7✔
1127
    }
1128
    arguments.next();
3✔
1129
    while (commandletIterator.hasNext()) {
3✔
1130
      Commandlet cmd = commandletIterator.next();
4✔
1131
      if (!arguments.current().isEnd()) {
4✔
1132
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1133
      }
1134
    }
1✔
1135
    return collector.getSortedCandidates();
3✔
1136
  }
1137

1138
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1139

1140
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
1141
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1142
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1143
    List<Property<?>> properties = cmd.getProperties();
3✔
1144
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1145
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1146
    for (Property<?> property : properties) {
10✔
1147
      if (property.isOption()) {
3✔
1148
        optionProperties.add(property);
4✔
1149
      }
1150
    }
1✔
1151
    CliArgument currentArgument = arguments.current();
3✔
1152
    while (!currentArgument.isEnd()) {
3✔
1153
      trace("Trying to match argument '{}'", currentArgument);
9✔
1154
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1155
        if (currentArgument.isCompletion()) {
3✔
1156
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1157
          while (optionIterator.hasNext()) {
3✔
1158
            Property<?> option = optionIterator.next();
4✔
1159
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1160
            if (success) {
2✔
1161
              optionIterator.remove();
2✔
1162
              arguments.next();
3✔
1163
            }
1164
          }
1✔
1165
        } else {
1✔
1166
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1167
          if (option != null) {
2✔
1168
            arguments.next();
3✔
1169
            boolean removed = optionProperties.remove(option);
4✔
1170
            if (!removed) {
2!
1171
              option = null;
×
1172
            }
1173
          }
1174
          if (option == null) {
2✔
1175
            trace("No such option was found.");
3✔
1176
            return;
1✔
1177
          }
1178
        }
1✔
1179
      } else {
1180
        if (valueIterator.hasNext()) {
3✔
1181
          Property<?> valueProperty = valueIterator.next();
4✔
1182
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1183
          if (!success) {
2✔
1184
            trace("Completion cannot match any further.");
3✔
1185
            return;
1✔
1186
          }
1187
        } else {
1✔
1188
          trace("No value left for completion.");
3✔
1189
          return;
1✔
1190
        }
1191
      }
1192
      currentArgument = arguments.current();
4✔
1193
    }
1194
  }
1✔
1195

1196
  /**
1197
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1198
   *     {@link CliArguments#copy() copy} as needed.
1199
   * @param cmd the potential {@link Commandlet} to match.
1200
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1201
   */
1202
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1203

1204
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1205
    CliArgument currentArgument = arguments.current();
3✔
1206
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1207
    Property<?> property = null;
2✔
1208
    if (propertyIterator.hasNext()) {
3!
1209
      property = propertyIterator.next();
4✔
1210
    }
1211
    while (!currentArgument.isEnd()) {
3✔
1212
      trace("Trying to match argument '{}'", currentArgument);
9✔
1213
      Property<?> currentProperty = property;
2✔
1214
      if (!arguments.isEndOptions()) {
3!
1215
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1216
        if (option != null) {
2!
1217
          currentProperty = option;
×
1218
        }
1219
      }
1220
      if (currentProperty == null) {
2!
1221
        trace("No option or next value found");
×
1222
        ValidationState state = new ValidationState(null);
×
1223
        state.addErrorMessage("No matching property found");
×
1224
        return state;
×
1225
      }
1226
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1227
      if (currentProperty == property) {
3!
1228
        if (!property.isMultiValued()) {
3✔
1229
          if (propertyIterator.hasNext()) {
3✔
1230
            property = propertyIterator.next();
5✔
1231
          } else {
1232
            property = null;
2✔
1233
          }
1234
        }
1235
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1236
          arguments.stopSplitShortOptions();
2✔
1237
        }
1238
      }
1239
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1240
      if (!matches) {
2!
1241
        ValidationState state = new ValidationState(null);
×
1242
        state.addErrorMessage("No matching property found");
×
1243
        return state;
×
1244
      }
1245
      currentArgument = arguments.current();
3✔
1246
    }
1✔
1247
    return ValidationResultValid.get();
2✔
1248
  }
1249

1250
  @Override
1251
  public String findBash() {
1252

1253
    String bash = "bash";
2✔
1254
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1255
      bash = findBashOnWindows();
×
1256
    }
1257

1258
    return bash;
2✔
1259
  }
1260

1261
  private String findBashOnWindows() {
1262

1263
    // Check if Git Bash exists in the default location
1264
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1265
    if (Files.exists(defaultPath)) {
×
1266
      return defaultPath.toString();
×
1267
    }
1268

1269
    // If not found in the default location, try the registry query
1270
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1271
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1272
    String regQueryResult;
1273
    for (String bashVariant : bashVariants) {
×
1274
      for (String registryKey : registryKeys) {
×
1275
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1276
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1277

1278
        try {
1279
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1280
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1281
            StringBuilder output = new StringBuilder();
×
1282
            String line;
1283

1284
            while ((line = reader.readLine()) != null) {
×
1285
              output.append(line);
×
1286
            }
1287

1288
            int exitCode = process.waitFor();
×
1289
            if (exitCode != 0) {
×
1290
              return null;
×
1291
            }
1292

1293
            regQueryResult = output.toString();
×
1294
            if (regQueryResult != null) {
×
1295
              int index = regQueryResult.indexOf("REG_SZ");
×
1296
              if (index != -1) {
×
1297
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1298
                return path + "\\bin\\bash.exe";
×
1299
              }
1300
            }
1301

1302
          }
×
1303
        } catch (Exception e) {
×
1304
          return null;
×
1305
        }
×
1306
      }
1307
    }
1308
    // no bash found
1309
    return null;
×
1310
  }
1311

1312
  @Override
1313
  public WindowsPathSyntax getPathSyntax() {
1314

1315
    return this.pathSyntax;
3✔
1316
  }
1317

1318
  /**
1319
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1320
   */
1321
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1322

1323
    this.pathSyntax = pathSyntax;
3✔
1324
  }
1✔
1325

1326
  /**
1327
   * @return the {@link IdeStartContextImpl}.
1328
   */
1329
  public IdeStartContextImpl getStartContext() {
1330

1331
    return startContext;
3✔
1332
  }
1333

1334
  /**
1335
   * @return the {@link WindowsHelper}.
1336
   */
1337
  public final WindowsHelper getWindowsHelper() {
1338

1339
    if (this.windowsHelper == null) {
3✔
1340
      this.windowsHelper = createWindowsHelper();
4✔
1341
    }
1342
    return this.windowsHelper;
3✔
1343
  }
1344

1345
  /**
1346
   * @return the new {@link WindowsHelper} instance.
1347
   */
1348
  protected WindowsHelper createWindowsHelper() {
1349

1350
    return new WindowsHelperImpl(this);
×
1351
  }
1352

1353
  /**
1354
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1355
   */
1356
  public void reload() {
1357

1358
    this.variables = null;
3✔
1359
    this.customToolRepository = null;
3✔
1360
  }
1✔
1361

1362
  @Override
1363
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1364

1365
    assert (Files.isDirectory(installationPath));
6!
1366
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1367
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1368
  }
1✔
1369

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