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

devonfw / IDEasy / 15728010215

18 Jun 2025 08:37AM UTC coverage: 67.733% (-0.03%) from 67.766%
15728010215

push

github

web-flow
#1340: proper fix if IDE_ROOT is not sane (#1350)

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

3168 of 5080 branches covered (62.36%)

Branch coverage included in aggregate %.

8121 of 11587 relevant lines covered (70.09%)

3.07 hits per line

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

61.45
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
    if (Files.isDirectory(workingDirectory)) {
5✔
163
      workingDirectory = this.fileAccess.toCanonicalPath(workingDirectory);
6✔
164
    } else {
165
      warning("Current working directory does not exist: {}", workingDirectory);
9✔
166
    }
167
    this.cwd = workingDirectory;
3✔
168
    // detect IDE_HOME and WORKSPACE
169
    Path currentDir = workingDirectory;
2✔
170
    String name1 = "";
2✔
171
    String name2 = "";
2✔
172
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
173
    while (currentDir != null) {
2✔
174
      trace("Looking for IDE_HOME in {}", currentDir);
9✔
175
      if (isIdeHome(currentDir)) {
4✔
176
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
177
          workspace = name2;
3✔
178
        }
179
        break;
180
      }
181
      name2 = name1;
2✔
182
      int nameCount = currentDir.getNameCount();
3✔
183
      if (nameCount >= 1) {
3✔
184
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
185
      }
186
      currentDir = currentDir.getParent();
3✔
187
      if ((ideRootPath != null) && (ideRootPath.equals(currentDir))) {
2!
188
        // prevent that during tests we traverse to the real IDE project of IDEasy developer
189
        currentDir = null;
×
190
      }
191
    }
1✔
192

193
    // detection completed, initializing variables
194
    this.ideRoot = findIdeRoot(currentDir);
5✔
195

196
    setCwd(workingDirectory, workspace, currentDir);
5✔
197

198
    if (this.ideRoot != null) {
3✔
199
      Path tempDownloadPath = getTempDownloadPath();
3✔
200
      if (Files.isDirectory(tempDownloadPath)) {
6✔
201
        // TODO delete all files older than 1 day here...
202
      } else {
203
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
204
      }
205
    }
206

207
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
208
    this.mavenRepository = new MavenRepository(this);
6✔
209
  }
1✔
210

211
  private Path findIdeRoot(Path ideHomePath) {
212

213
    Path ideRootPath = null;
2✔
214
    if (ideHomePath != null) {
2✔
215
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
216
      ideRootPath = ideHomePath.getParent();
3✔
217
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
8!
218
        warning(
12✔
219
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
220
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
221
            ideRootPathFromEnv,
222
            ideHomePath.getFileName(), ideRootPath);
6✔
223
      }
224

225
    } else if (!isTest()) {
4!
226
      ideRootPath = getIdeRootPathFromEnv(true);
×
227
    }
228
    return ideRootPath;
2✔
229
  }
230

231
  /**
232
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
233
   */
234
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
235

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

267
  @Override
268
  public void setCwd(Path userDir, String workspace, Path ideHome) {
269

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

298
    this.path = computeSystemPath();
4✔
299
  }
1✔
300

301
  private String getMessageIdeHomeFound() {
302

303
    return "IDE environment variables have been set for " + this.ideHome + " in workspace " + this.workspaceName;
7✔
304
  }
305

306
  private String getMessageNotInsideIdeProject() {
307

308
    return "You are not inside an IDE project: " + this.cwd;
5✔
309
  }
310

311
  private String getMessageIdeRootNotFound() {
312

313
    String root = getSystem().getEnv("IDE_ROOT");
5✔
314
    if (root == null) {
2!
315
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
316
    } else {
317
      return "The environment variable IDE_ROOT is pointing to an invalid path " + root + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
×
318
    }
319
  }
320

321
  /**
322
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
323
   */
324
  public boolean isTest() {
325

326
    return false;
×
327
  }
328

329
  protected SystemPath computeSystemPath() {
330

331
    return new SystemPath(this);
×
332
  }
333

334
  private boolean isIdeHome(Path dir) {
335

336
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
337
      return false;
2✔
338
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
339
      return false;
×
340
    }
341
    return true;
2✔
342
  }
343

344
  private EnvironmentVariables createVariables() {
345

346
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
347
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
348
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
349
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
350
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
351
    return conf.resolved();
3✔
352
  }
353

354
  protected AbstractEnvironmentVariables createSystemVariables() {
355

356
    return EnvironmentVariables.ofSystem(this);
3✔
357
  }
358

359
  @Override
360
  public SystemInfo getSystemInfo() {
361

362
    return this.systemInfo;
3✔
363
  }
364

365
  @Override
366
  public FileAccess getFileAccess() {
367

368
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
369
    configureNetworkProxy();
2✔
370
    return this.fileAccess;
3✔
371
  }
372

373
  @Override
374
  public CommandletManager getCommandletManager() {
375

376
    return this.commandletManager;
3✔
377
  }
378

379
  @Override
380
  public ToolRepository getDefaultToolRepository() {
381

382
    return this.defaultToolRepository;
3✔
383
  }
384

385
  @Override
386
  public MavenRepository getMavenToolRepository() {
387

388
    return this.mavenRepository;
3✔
389
  }
390

391
  @Override
392
  public CustomToolRepository getCustomToolRepository() {
393

394
    if (this.customToolRepository == null) {
3!
395
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
396
    }
397
    return this.customToolRepository;
3✔
398
  }
399

400
  @Override
401
  public Path getIdeHome() {
402

403
    return this.ideHome;
3✔
404
  }
405

406
  @Override
407
  public String getProjectName() {
408

409
    if (this.ideHome != null) {
3!
410
      return this.ideHome.getFileName().toString();
5✔
411
    }
412
    return "";
×
413
  }
414

415
  @Override
416
  public VersionIdentifier getProjectVersion() {
417

418
    if (this.ideHome != null) {
3!
419
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
420
      if (Files.exists(versionFile)) {
5✔
421
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
422
        return VersionIdentifier.of(version);
3✔
423
      }
424
    }
425
    return IdeMigrator.START_VERSION;
2✔
426
  }
427

428
  @Override
429
  public void setProjectVersion(VersionIdentifier version) {
430

431
    if (this.ideHome == null) {
3!
432
      throw new IllegalStateException("IDE_HOME not available!");
×
433
    }
434
    Objects.requireNonNull(version);
3✔
435
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
436
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
437
  }
1✔
438

439
  @Override
440
  public Path getIdeRoot() {
441

442
    return this.ideRoot;
3✔
443
  }
444

445
  @Override
446
  public Path getIdePath() {
447

448
    Path myIdeRoot = getIdeRoot();
3✔
449
    if (myIdeRoot == null) {
2!
450
      return null;
×
451
    }
452
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
453
  }
454

455
  @Override
456
  public Path getCwd() {
457

458
    return this.cwd;
3✔
459
  }
460

461
  @Override
462
  public Path getTempPath() {
463

464
    Path idePath = getIdePath();
3✔
465
    if (idePath == null) {
2!
466
      return null;
×
467
    }
468
    return idePath.resolve("tmp");
4✔
469
  }
470

471
  @Override
472
  public Path getTempDownloadPath() {
473

474
    Path tmp = getTempPath();
3✔
475
    if (tmp == null) {
2!
476
      return null;
×
477
    }
478
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
479
  }
480

481
  @Override
482
  public Path getUserHome() {
483

484
    return this.userHome;
3✔
485
  }
486

487
  @Override
488
  public Path getUserHomeIde() {
489

490
    return this.userHomeIde;
3✔
491
  }
492

493
  @Override
494
  public Path getSettingsPath() {
495

496
    return this.settingsPath;
3✔
497
  }
498

499
  @Override
500
  public Path getSettingsGitRepository() {
501

502
    Path settingsPath = getSettingsPath();
3✔
503
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
504
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
505
      error("Settings repository exists but is not a git repository.");
3✔
506
      return null;
2✔
507
    }
508
    return settingsPath;
2✔
509
  }
510

511
  @Override
512
  public boolean isSettingsRepositorySymlinkOrJunction() {
513

514
    Path settingsPath = getSettingsPath();
3✔
515
    if (settingsPath == null) {
2!
516
      return false;
×
517
    }
518
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
519
  }
520

521
  @Override
522
  public Path getSettingsCommitIdPath() {
523

524
    return this.settingsCommitIdPath;
3✔
525
  }
526

527
  @Override
528
  public Path getConfPath() {
529

530
    return this.confPath;
3✔
531
  }
532

533
  @Override
534
  public Path getSoftwarePath() {
535

536
    if (this.ideHome == null) {
3✔
537
      return null;
2✔
538
    }
539
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
540
  }
541

542
  @Override
543
  public Path getSoftwareExtraPath() {
544

545
    Path softwarePath = getSoftwarePath();
3✔
546
    if (softwarePath == null) {
2!
547
      return null;
×
548
    }
549
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
550
  }
551

552
  @Override
553
  public Path getSoftwareRepositoryPath() {
554

555
    Path idePath = getIdePath();
3✔
556
    if (idePath == null) {
2!
557
      return null;
×
558
    }
559
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
560
  }
561

562
  @Override
563
  public Path getPluginsPath() {
564

565
    return this.pluginsPath;
3✔
566
  }
567

568
  @Override
569
  public String getWorkspaceName() {
570

571
    return this.workspaceName;
3✔
572
  }
573

574
  @Override
575
  public Path getWorkspacePath() {
576

577
    return this.workspacePath;
3✔
578
  }
579

580
  @Override
581
  public Path getDownloadPath() {
582

583
    return this.downloadPath;
3✔
584
  }
585

586
  @Override
587
  public Path getUrlsPath() {
588

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

596
  @Override
597
  public Path getToolRepositoryPath() {
598

599
    Path idePath = getIdePath();
3✔
600
    if (idePath == null) {
2!
601
      return null;
×
602
    }
603
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
604
  }
605

606
  @Override
607
  public SystemPath getPath() {
608

609
    return this.path;
3✔
610
  }
611

612
  @Override
613
  public EnvironmentVariables getVariables() {
614

615
    if (this.variables == null) {
3✔
616
      this.variables = createVariables();
4✔
617
    }
618
    return this.variables;
3✔
619
  }
620

621
  @Override
622
  public UrlMetadata getUrls() {
623

624
    if (this.urlMetadata == null) {
3✔
625
      if (!isTest()) {
3!
626
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
627
      }
628
      this.urlMetadata = new UrlMetadata(this);
6✔
629
    }
630
    return this.urlMetadata;
3✔
631
  }
632

633
  @Override
634
  public boolean isQuietMode() {
635

636
    return this.startContext.isQuietMode();
4✔
637
  }
638

639
  @Override
640
  public boolean isBatchMode() {
641

642
    return this.startContext.isBatchMode();
4✔
643
  }
644

645
  @Override
646
  public boolean isForceMode() {
647

648
    return this.startContext.isForceMode();
4✔
649
  }
650

651
  @Override
652
  public boolean isForcePull() {
653

654
    return this.startContext.isForcePull();
×
655
  }
656

657
  @Override
658
  public boolean isForcePlugins() {
659

660
    return this.startContext.isForcePlugins();
×
661
  }
662

663
  @Override
664
  public boolean isForceRepositories() {
665

666
    return this.startContext.isForceRepositories();
×
667
  }
668

669
  @Override
670
  public boolean isOfflineMode() {
671

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

675
  @Override
676
  public boolean isSkipUpdatesMode() {
677

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

681
  @Override
682
  public boolean isOnline() {
683

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

703
  private void configureNetworkProxy() {
704

705
    if (this.networkProxy == null) {
3✔
706
      this.networkProxy = new NetworkProxy(this);
6✔
707
      this.networkProxy.configure();
3✔
708
    }
709
  }
1✔
710

711
  @Override
712
  public Locale getLocale() {
713

714
    Locale locale = this.startContext.getLocale();
4✔
715
    if (locale == null) {
2✔
716
      locale = Locale.getDefault();
2✔
717
    }
718
    return locale;
2✔
719
  }
720

721
  @Override
722
  public DirectoryMerger getWorkspaceMerger() {
723

724
    if (this.workspaceMerger == null) {
3✔
725
      this.workspaceMerger = new DirectoryMerger(this);
6✔
726
    }
727
    return this.workspaceMerger;
3✔
728
  }
729

730
  /**
731
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
732
   */
733
  @Override
734
  public Path getDefaultExecutionDirectory() {
735

736
    return this.defaultExecutionDirectory;
×
737
  }
738

739
  /**
740
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
741
   */
742
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
743

744
    if (defaultExecutionDirectory != null) {
×
745
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
746
    }
747
  }
×
748

749
  @Override
750
  public GitContext getGitContext() {
751

752
    return new GitContextImpl(this);
×
753
  }
754

755
  @Override
756
  public ProcessContext newProcess() {
757

758
    ProcessContext processContext = createProcessContext();
3✔
759
    if (this.defaultExecutionDirectory != null) {
3!
760
      processContext.directory(this.defaultExecutionDirectory);
×
761
    }
762
    return processContext;
2✔
763
  }
764

765
  @Override
766
  public IdeSystem getSystem() {
767

768
    if (this.system == null) {
×
769
      this.system = new IdeSystemImpl(this);
×
770
    }
771
    return this.system;
×
772
  }
773

774
  /**
775
   * @return a new instance of {@link ProcessContext}.
776
   * @see #newProcess()
777
   */
778
  protected ProcessContext createProcessContext() {
779

780
    return new ProcessContextImpl(this);
5✔
781
  }
782

783
  @Override
784
  public IdeSubLogger level(IdeLogLevel level) {
785

786
    return this.startContext.level(level);
5✔
787
  }
788

789
  @Override
790
  public void logIdeHomeAndRootStatus() {
791

792
    if (this.ideRoot != null) {
3!
793
      success("IDE_ROOT is set to {}", this.ideRoot);
×
794
    }
795
    if (this.ideHome == null) {
3!
796
      warning(getMessageNotInsideIdeProject());
5✔
797
    } else {
798
      success("IDE_HOME is set to {}", this.ideHome);
×
799
    }
800
  }
1✔
801

802
  @Override
803
  public String askForInput(String message, String defaultValue) {
804

805
    if (!message.isBlank()) {
×
806
      info(message);
×
807
    }
808
    if (isBatchMode()) {
×
809
      if (isForceMode() || isForcePull()) {
×
810
        return defaultValue;
×
811
      } else {
812
        throw new CliAbortException();
×
813
      }
814
    }
815
    String input = readLine().trim();
×
816
    return input.isEmpty() ? defaultValue : input;
×
817
  }
818

819
  @Override
820
  public String askForInput(String message) {
821

822
    String input;
823
    do {
824
      info(message);
3✔
825
      input = readLine().trim();
4✔
826
    } while (input.isEmpty());
3!
827

828
    return input;
2✔
829
  }
830

831
  @SuppressWarnings("unchecked")
832
  @Override
833
  public <O> O question(String question, O... options) {
834

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

869
  /**
870
   * @return the input from the end-user (e.g. read from the console).
871
   */
872
  protected abstract String readLine();
873

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

876
    O duplicate = mapping.put(key, option);
×
877
    if (duplicate != null) {
×
878
      throw new IllegalArgumentException("Duplicated option " + key);
×
879
    }
880
  }
×
881

882
  @Override
883
  public Step getCurrentStep() {
884

885
    return this.currentStep;
×
886
  }
887

888
  @Override
889
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
890

891
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
892
    return this.currentStep;
3✔
893
  }
894

895
  /**
896
   * Internal method to end the running {@link Step}.
897
   *
898
   * @param step the current {@link Step} to end.
899
   */
900
  public void endStep(StepImpl step) {
901

902
    if (step == this.currentStep) {
4!
903
      this.currentStep = this.currentStep.getParent();
6✔
904
    } else {
905
      String currentStepName = "null";
×
906
      if (this.currentStep != null) {
×
907
        currentStepName = this.currentStep.getName();
×
908
      }
909
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
910
    }
911
  }
1✔
912

913
  /**
914
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
915
   *
916
   * @param arguments the {@link CliArgument}.
917
   * @return the return code of the execution.
918
   */
919
  public int run(CliArguments arguments) {
920

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

961
  @Override
962
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
963

964
    this.startContext.deactivateLogging(threshold);
4✔
965
    lambda.run();
2✔
966
    this.startContext.activateLogging();
3✔
967
  }
1✔
968

969
  /**
970
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
971
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
972
   *     {@link Commandlet} did not match and we have to try a different candidate).
973
   */
974
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
975

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

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

1033
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1034

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

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

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

1113
  /**
1114
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1115
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1116
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1117
   */
1118
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1119

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

1146
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1147

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

1204
  /**
1205
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1206
   *     {@link CliArguments#copy() copy} as needed.
1207
   * @param cmd the potential {@link Commandlet} to match.
1208
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1209
   */
1210
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1211

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

1258
  @Override
1259
  public String findBash() {
1260

1261
    String bash = "bash";
2✔
1262
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1263
      bash = findBashOnWindows();
×
1264
    }
1265

1266
    return bash;
2✔
1267
  }
1268

1269
  private String findBashOnWindows() {
1270

1271
    // Check if Git Bash exists in the default location
1272
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1273
    if (Files.exists(defaultPath)) {
×
1274
      return defaultPath.toString();
×
1275
    }
1276

1277
    // If not found in the default location, try the registry query
1278
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1279
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1280
    String regQueryResult;
1281
    for (String bashVariant : bashVariants) {
×
1282
      for (String registryKey : registryKeys) {
×
1283
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1284
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1285

1286
        try {
1287
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1288
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1289
            StringBuilder output = new StringBuilder();
×
1290
            String line;
1291

1292
            while ((line = reader.readLine()) != null) {
×
1293
              output.append(line);
×
1294
            }
1295

1296
            int exitCode = process.waitFor();
×
1297
            if (exitCode != 0) {
×
1298
              return null;
×
1299
            }
1300

1301
            regQueryResult = output.toString();
×
1302
            if (regQueryResult != null) {
×
1303
              int index = regQueryResult.indexOf("REG_SZ");
×
1304
              if (index != -1) {
×
1305
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1306
                return path + "\\bin\\bash.exe";
×
1307
              }
1308
            }
1309

1310
          }
×
1311
        } catch (Exception e) {
×
1312
          return null;
×
1313
        }
×
1314
      }
1315
    }
1316
    // no bash found
1317
    return null;
×
1318
  }
1319

1320
  @Override
1321
  public WindowsPathSyntax getPathSyntax() {
1322

1323
    return this.pathSyntax;
3✔
1324
  }
1325

1326
  /**
1327
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1328
   */
1329
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1330

1331
    this.pathSyntax = pathSyntax;
3✔
1332
  }
1✔
1333

1334
  /**
1335
   * @return the {@link IdeStartContextImpl}.
1336
   */
1337
  public IdeStartContextImpl getStartContext() {
1338

1339
    return startContext;
3✔
1340
  }
1341

1342
  /**
1343
   * @return the {@link WindowsHelper}.
1344
   */
1345
  public final WindowsHelper getWindowsHelper() {
1346

1347
    if (this.windowsHelper == null) {
3✔
1348
      this.windowsHelper = createWindowsHelper();
4✔
1349
    }
1350
    return this.windowsHelper;
3✔
1351
  }
1352

1353
  /**
1354
   * @return the new {@link WindowsHelper} instance.
1355
   */
1356
  protected WindowsHelper createWindowsHelper() {
1357

1358
    return new WindowsHelperImpl(this);
×
1359
  }
1360

1361
  /**
1362
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1363
   */
1364
  public void reload() {
1365

1366
    this.variables = null;
3✔
1367
    this.customToolRepository = null;
3✔
1368
  }
1✔
1369

1370
  @Override
1371
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1372

1373
    assert (Files.isDirectory(installationPath));
6!
1374
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1375
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1376
  }
1✔
1377

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